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
108 changed files with 4119 additions and 4708 deletions

2
.env
View File

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

View File

@@ -36,3 +36,57 @@ MONTONIO_API_URL=https://sandbox-stargate.montonio.com
# JOBS # JOBS
JOBS_API_TOKEN=73ce073c-6dd4-11f0-8e75-8fee89786197 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. ## PUBLIC KEYS OR CONFIGURATION ARE OKAY TO BE PLACED HERE.
# SUPABASE # SUPABASE
# NEXT_PUBLIC_SUPABASE_URL=https://oqsdacktkhmbylmzstjq.supabase.co NEXT_PUBLIC_SUPABASE_URL=https://oqsdacktkhmbylmzstjq.supabase.co
# NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Im9xc2RhY2t0a2htYnlsbXpzdGpxIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NDY1MjgxMjMsImV4cCI6MjA2MjEwNDEyM30.LdHCTWxijFmhXdnT9KVuLRAVbtSwY7OO-oLtpd8GmO0 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 # MONTONIO
# NEXT_PUBLIC_MONTONIO_ACCESS_KEY=7da5d7fa-3383-4997-9435-46aa818f4ead 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. ## PUBLIC KEYS OR CONFIGURATION ARE OKAY TO BE PLACED HERE.
# SUPABASE # SUPABASE
# NEXT_PUBLIC_SUPABASE_URL=https://klocrucggryikaxzvxgc.supabase.co NEXT_PUBLIC_SUPABASE_URL=https://kaldvociniytdbbcxvqk.supabase.co
# NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Imtsb2NydWNnZ3J5aWtheHp2eGdjIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTY5ODQ2MjgsImV4cCI6MjA3MjU2MDYyOH0.2XOQngowcymiSUZO_XEEWAWzco2uRIjwG7TAeRRLIdU 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 # MONTONIO
# NEXT_PUBLIC_MONTONIO_ACCESS_KEY=7da5d7fa-3383-4997-9435-46aa818f4ead NEXT_PUBLIC_MONTONIO_ACCESS_KEY=7da5d7fa-3383-4997-9435-46aa818f4ead

View File

@@ -19,4 +19,6 @@ EMAIL_PASSWORD=password
# STRIPE # STRIPE
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_51K9cWKI1i3VnbZTq2HGstY2S8wt3peF1MOqPXFO4LR8ln2QgS7GxL8XyKaKLvn7iFHeqAnvdDw0o48qN7rrwwcHU00jOtKhjsf 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

@@ -22,12 +22,12 @@ COPY . .
ENV NODE_ENV=production ENV NODE_ENV=production
RUN set -a \ RUN set -a \
&& . .env \ && . .env \
&& . .env.production \ && . .env.production \
&& . .env.staging \ && . .env.staging \
&& set +a \ && set +a \
&& node check-env.js \ && node check-env.js \
&& pnpm build && pnpm build
# --- Stage 2: Runtime --- # --- Stage 2: Runtime ---
@@ -41,13 +41,13 @@ COPY --from=builder /app ./
RUN cp ".env.${APP_ENV}" .env.local RUN cp ".env.${APP_ENV}" .env.local
RUN npm install -g pnpm@9 \ RUN npm install -g pnpm@9 \
&& pnpm install --prod --frozen-lockfile && pnpm install --prod --frozen-lockfile
ENV NODE_ENV=production ENV NODE_ENV=production
# 🔍 Optional: Log key envs for debug # 🔍 Optional: Log key envs for debug
RUN echo "📄 .env contents:" && cat .env.local \ RUN echo "📄 .env contents:" && cat .env.local \
&& echo "🔧 Current ENV available to Next.js build:" && printenv | grep -E 'SUPABASE|STRIPE|NEXT|NODE_ENV' || true && echo "🔧 Current ENV available to Next.js build:" && printenv | grep -E 'SUPABASE|STRIPE|NEXT|NODE_ENV' || true
EXPOSE 3000 EXPOSE 3000

View File

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

View File

@@ -1,7 +1,7 @@
import { NextRequest, NextResponse } from "next/server"; import { NextRequest, NextResponse } from "next/server";
import loadEnv from "../handler/load-env"; import loadEnv from "../handler/load-env";
import validateApiKey from "../handler/validate-api-key"; 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 { retrieveOrder } from "@lib/data/orders";
import { getMedipostDispatchTries } from "~/lib/services/audit.service"; import { getMedipostDispatchTries } from "~/lib/services/audit.service";
@@ -25,7 +25,7 @@ export const POST = async (request: NextRequest) => {
try { try {
const medusaOrder = await retrieveOrder(medusaOrderId); const medusaOrder = await retrieveOrder(medusaOrderId);
const orderedAnalysisElements = await getOrderedAnalysisIds({ medusaOrder }); const orderedAnalysisElements = await getOrderedAnalysisElementsIds({ medusaOrder });
await sendOrderToMedipost({ medusaOrderId, orderedAnalysisElements }); await sendOrderToMedipost({ medusaOrderId, orderedAnalysisElements });
console.info("Successfully sent order to medipost"); console.info("Successfully sent order to medipost");
return NextResponse.json({ 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 { composeOrderTestResponseXML, sendPrivateMessageTestResponse } from "~/lib/services/medipostTest.service";
import { retrieveOrder } from "@lib/data"; import { retrieveOrder } from "@lib/data";
import { getAccountAdmin } from "~/lib/services/account.service"; 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 loadEnv from "../handler/load-env";
import validateApiKey from "../handler/validate-api-key"; import validateApiKey from "../handler/validate-api-key";
@@ -24,7 +24,7 @@ export async function POST(request: NextRequest) {
const medusaOrder = await retrieveOrder(medusaOrderId) const medusaOrder = await retrieveOrder(medusaOrderId)
const account = await getAccountAdmin({ primaryOwnerUserId: medreportOrder.user_id }); 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} ordered analysis elements`);
const idsToSend = orderedAnalysisElementsIds; const idsToSend = orderedAnalysisElementsIds;
@@ -35,8 +35,8 @@ export async function POST(request: NextRequest) {
lastName: account.last_name ?? '', lastName: account.last_name ?? '',
phone: account.phone ?? '', phone: account.phone ?? '',
}, },
orderedAnalysisElementsIds: idsToSend.map(({ analysisElementId }) => analysisElementId).filter(Boolean) as number[], orderedAnalysisElementsIds: idsToSend.map(({ analysisElementId }) => analysisElementId),
orderedAnalysesIds: idsToSend.map(({ analysisId }) => analysisId).filter(Boolean) as number[], orderedAnalysesIds: [],
orderId: medusaOrderId, orderId: medusaOrderId,
orderCreatedAt: new Date(medreportOrder.created_at), 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 { composeOrderTestResponseXML, sendPrivateMessageTestResponse } from "~/lib/services/medipostTest.service";
import { retrieveOrder } from "@lib/data"; import { retrieveOrder } from "@lib/data";
import { getAccountAdmin } from "~/lib/services/account.service"; 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) { export async function POST(request: Request) {
// const isDev = process.env.NODE_ENV === 'development'; // 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 }); // 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 medusaOrder = await retrieveOrder(medusaOrderId)
const medreportOrder = await getOrder({ medusaOrderId }); const medreportOrder = await getOrder({ medusaOrderId });
const account = await getAccountAdmin({ primaryOwnerUserId: medreportOrder.user_id }); 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({ const messageXml = await composeOrderTestResponseXML({
person: { person: {
idCode: account.personal_code!, idCode: account.personal_code!,
@@ -27,8 +28,8 @@ export async function POST(request: Request) {
lastName: account.last_name ?? '', lastName: account.last_name ?? '',
phone: account.phone ?? '', phone: account.phone ?? '',
}, },
orderedAnalysisElementsIds: orderedAnalysisElementsIds.map(({ analysisElementId }) => analysisElementId).filter(Boolean) as number[], orderedAnalysisElementsIds: idsToSend.map(({ analysisElementId }) => analysisElementId),
orderedAnalysesIds: orderedAnalysisElementsIds.map(({ analysisId }) => analysisId).filter(Boolean) as number[], orderedAnalysesIds: [],
orderId: medusaOrderId, orderId: medusaOrderId,
orderCreatedAt: new Date(medreportOrder.created_at), orderCreatedAt: new Date(medreportOrder.created_at),
}); });

View File

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

View File

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

View File

@@ -1,11 +1,17 @@
import { use } from 'react'; import { use } from 'react';
import { cookies } from 'next/headers';
import { retrieveCart } from '@lib/data/cart'; import { retrieveCart } from '@lib/data/cart';
import { StoreCart } from '@medusajs/types'; import { StoreCart } from '@medusajs/types';
import { z } from 'zod';
import { UserWorkspaceContextProvider } from '@kit/accounts/components'; import { UserWorkspaceContextProvider } from '@kit/accounts/components';
import { AppLogo } from '@kit/shared/components/app-logo'; import { AppLogo } from '@kit/shared/components/app-logo';
import { pathsConfig } from '@kit/shared/config'; import {
pathsConfig,
personalAccountNavigationConfig,
} from '@kit/shared/config';
import { Page, PageMobileNavigation, PageNavigation } from '@kit/ui/page'; import { Page, PageMobileNavigation, PageNavigation } from '@kit/ui/page';
import { SidebarProvider } from '@kit/ui/shadcn-sidebar'; import { SidebarProvider } from '@kit/ui/shadcn-sidebar';
@@ -18,11 +24,40 @@ import { HomeSidebar } from '../_components/home-sidebar';
import { loadUserWorkspace } from '../_lib/server/load-user-workspace'; import { loadUserWorkspace } from '../_lib/server/load-user-workspace';
function UserHomeLayout({ children }: React.PropsWithChildren) { function UserHomeLayout({ children }: React.PropsWithChildren) {
const state = use(getLayoutState());
if (state.style === 'sidebar') {
return <SidebarLayout>{children}</SidebarLayout>;
}
return <HeaderLayout>{children}</HeaderLayout>; return <HeaderLayout>{children}</HeaderLayout>;
} }
export default withI18n(UserHomeLayout); export default withI18n(UserHomeLayout);
function SidebarLayout({ children }: React.PropsWithChildren) {
const workspace = use(loadUserWorkspace());
const state = use(getLayoutState());
return (
<UserWorkspaceContextProvider value={workspace}>
<SidebarProvider defaultOpen={state.open}>
<Page style={'sidebar'}>
<PageNavigation>
<HomeSidebar />
</PageNavigation>
<PageMobileNavigation className={'flex items-center justify-between'}>
<MobileNavigation workspace={workspace} cart={null} />
</PageMobileNavigation>
{children}
</Page>
</SidebarProvider>
</UserWorkspaceContextProvider>
);
}
function HeaderLayout({ children }: React.PropsWithChildren) { function HeaderLayout({ children }: React.PropsWithChildren) {
const workspace = use(loadUserWorkspace()); const workspace = use(loadUserWorkspace());
const cart = use(retrieveCart()); const cart = use(retrieveCart());
@@ -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,7 +1,6 @@
"use client" "use client"
import { Badge, Text } from "@medusajs/ui" import { Badge, Heading, Text } from "@medusajs/ui"
import { toast } from '@kit/ui/sonner';
import React, { useActionState } from "react"; import React, { useActionState } from "react";
import { applyPromotions, submitPromotionForm } from "@lib/data/cart" import { applyPromotions, submitPromotionForm } from "@lib/data/cart"
@@ -32,19 +31,11 @@ export default function DiscountCode({ cart }: {
const removePromotionCode = async (code: string) => { const removePromotionCode = async (code: string) => {
const validPromotions = promotions.filter( const validPromotions = promotions.filter(
(promotion) => promotion.code !== code, (promotion) => promotion.code !== code
) )
await applyPromotions( await applyPromotions(
validPromotions.filter((p) => p.code === undefined).map((p) => p.code!), validPromotions.filter((p) => p.code === undefined).map((p) => p.code!)
{
onSuccess: () => {
toast.success(t('cart:discountCode.removeSuccess'));
},
onError: () => {
toast.error(t('cart:discountCode.removeError'));
},
}
) )
} }
@@ -54,14 +45,7 @@ export default function DiscountCode({ cart }: {
.map((p) => p.code!) .map((p) => p.code!)
codes.push(code.toString()) codes.push(code.toString())
await applyPromotions(codes, { await applyPromotions(codes)
onSuccess: () => {
toast.success(t('cart:discountCode.addSuccess'));
},
onError: () => {
toast.error(t('cart:discountCode.addError'));
},
});
form.reset() form.reset()
} }
@@ -80,7 +64,7 @@ export default function DiscountCode({ cart }: {
<Form {...form}> <Form {...form}>
<form <form
onSubmit={form.handleSubmit((data) => addPromotionCode(data.code))} onSubmit={form.handleSubmit((data) => addPromotionCode(data.code))}
className="w-full mb-2 flex gap-x-2 sm:flex-row flex-col gap-y-2" className="w-full mb-2 flex gap-x-2"
> >
<FormField <FormField
name={'code'} name={'code'}
@@ -103,12 +87,16 @@ export default function DiscountCode({ cart }: {
</form> </form>
</Form> </Form>
{promotions.length > 0 ? ( <p className="text-sm text-muted-foreground">
<div className="w-full flex items-center mt-4"> <Trans i18nKey={'cart:discountCode.subtitle'} />
<div className="flex flex-col w-full gap-y-2"> </p>
<p>
<Trans i18nKey={'cart:discountCode.appliedCodes'} /> {promotions.length > 0 && (
</p> <div className="w-full flex items-center">
<div className="flex flex-col w-full">
<Heading className="txt-medium mb-2">
Promotion(s) applied:
</Heading>
{promotions.map((promotion) => { {promotions.map((promotion) => {
return ( return (
@@ -122,7 +110,6 @@ export default function DiscountCode({ cart }: {
<Badge <Badge
color={promotion.is_automatic ? "green" : "grey"} color={promotion.is_automatic ? "green" : "grey"}
size="small" size="small"
className="px-4"
> >
{promotion.code} {promotion.code}
</Badge>{" "} </Badge>{" "}
@@ -164,7 +151,7 @@ export default function DiscountCode({ cart }: {
> >
<Trash size={14} /> <Trash size={14} />
<span className="sr-only"> <span className="sr-only">
<Trans i18nKey={'cart:discountCode.remove'} /> Remove discount code from order
</span> </span>
</button> </button>
)} )}
@@ -173,10 +160,6 @@ export default function DiscountCode({ cart }: {
})} })}
</div> </div>
</div> </div>
) : (
<p className="text-sm text-muted-foreground">
<Trans i18nKey={'cart:discountCode.subtitle'} />
</p>
)} )}
</div> </div>
) )

View File

@@ -18,7 +18,7 @@ import { useTranslation } from "react-i18next";
import { handleNavigateToPayment } from "@/lib/services/medusaCart.service"; import { handleNavigateToPayment } from "@/lib/services/medusaCart.service";
import AnalysisLocation from "./analysis-location"; import AnalysisLocation from "./analysis-location";
const IS_DISCOUNT_SHOWN = true as boolean; const IS_DISCOUNT_SHOWN = false as boolean;
export default function Cart({ export default function Cart({
cart, cart,
@@ -69,7 +69,7 @@ export default function Cart({
const hasCartItems = Array.isArray(cart.items) && cart.items.length > 0; const hasCartItems = Array.isArray(cart.items) && cart.items.length > 0;
const isLocationsShown = synlabAnalyses.length > 0; const isLocationsShown = synlabAnalyses.length > 0;
return ( return (
<div className="grid grid-cols-1 small:grid-cols-[1fr_360px] gap-x-40 lg:px-4"> <div className="grid grid-cols-1 small:grid-cols-[1fr_360px] gap-x-40 lg:px-4">
<div className="flex flex-col bg-white gap-y-6"> <div className="flex flex-col bg-white gap-y-6">
@@ -77,62 +77,28 @@ export default function Cart({
<CartItems cart={cart} items={ttoServiceItems} productColumnLabelKey="cart:items.ttoServices.productColumnLabel" /> <CartItems cart={cart} items={ttoServiceItems} productColumnLabelKey="cart:items.ttoServices.productColumnLabel" />
</div> </div>
{hasCartItems && ( {hasCartItems && (
<> <div className="flex justify-end gap-x-4 px-6 py-4">
<div className="flex justify-end gap-x-4 px-6 pt-4"> <div className="mr-[36px]">
<div className="mr-[36px]"> <p className="ml-0 font-bold text-sm">
<p className="ml-0 font-bold text-sm text-muted-foreground"> <Trans i18nKey="cart:total" />
<Trans i18nKey="cart:subtotal" /> </p>
</p>
</div>
<div className="mr-[116px]">
<p className="text-sm">
{formatCurrency({
value: cart.subtotal,
currencyCode: cart.currency_code,
locale: language,
})}
</p>
</div>
</div> </div>
<div className="flex justify-end gap-x-4 px-6 py-2"> <div className="mr-[116px]">
<div className="mr-[36px]"> <p className="text-sm">
<p className="ml-0 font-bold text-sm text-muted-foreground"> {formatCurrency({
<Trans i18nKey="cart:promotionsTotal" /> value: cart.total,
</p> currencyCode: cart.currency_code,
</div> locale: language,
<div className="mr-[116px]"> })}
<p className="text-sm"> </p>
{formatCurrency({
value: cart.discount_total,
currencyCode: cart.currency_code,
locale: language,
})}
</p>
</div>
</div> </div>
<div className="flex justify-end gap-x-4 px-6"> </div>
<div className="mr-[36px]">
<p className="ml-0 font-bold text-sm">
<Trans i18nKey="cart:total" />
</p>
</div>
<div className="mr-[116px]">
<p className="text-sm">
{formatCurrency({
value: cart.total,
currencyCode: cart.currency_code,
locale: language,
})}
</p>
</div>
</div>
</>
)} )}
<div className="flex sm:flex-row flex-col gap-y-6 py-8 gap-x-4"> <div className="flex gap-y-6 py-8">
{IS_DISCOUNT_SHOWN && ( {IS_DISCOUNT_SHOWN && (
<Card <Card
className="flex flex-col justify-between w-full sm:w-1/2" className="flex flex-col justify-between w-1/2"
> >
<CardHeader className="pb-4"> <CardHeader className="pb-4">
<h5> <h5>
@@ -147,7 +113,7 @@ export default function Cart({
{isLocationsShown && ( {isLocationsShown && (
<Card <Card
className="flex flex-col justify-between w-full sm:w-1/2" className="flex flex-col justify-between w-1/2"
> >
<CardHeader className="pb-4"> <CardHeader className="pb-4">
<h5> <h5>

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 { BlendingModeIcon, RulerHorizontalIcon } from '@radix-ui/react-icons';
import { import {
Activity, Activity,
ChevronRight,
Clock9, Clock9,
Droplets,
Pill, Pill,
Scale, Scale,
TrendingUp, TrendingUp,
User, User,
} from 'lucide-react'; } 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 { getPersonParameters } from '@kit/shared/utils';
import { Button } from '@kit/ui/button'; import { Button } from '@kit/ui/button';
import { import {
Card, Card,
CardContent,
CardDescription, CardDescription,
CardFooter, CardFooter,
CardHeader, CardHeader,
CardProps,
} from '@kit/ui/card'; } from '@kit/ui/card';
import { Trans } from '@kit/ui/trans'; import { Trans } from '@kit/ui/trans';
import { cn } from '@kit/ui/utils'; import { cn } from '@kit/ui/utils';
import { isNil } from 'lodash';
import { BmiCategory } from '~/lib/types/bmi'; import { BmiCategory } from '~/lib/types/bmi';
import { import {
bmiFromMetric, bmiFromMetric,
getBmiBackgroundColor, getBmiBackgroundColor,
getBmiStatus, getBmiStatus,
} from '~/lib/utils'; } 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 = ({ const cards = ({
gender, gender,
@@ -49,91 +41,111 @@ const cards = ({
height, height,
weight, weight,
bmiStatus, bmiStatus,
smoking,
}: { }: {
gender?: string; gender?: string;
age?: number; age?: number;
height?: number | null; height?: number | null;
weight?: number | null; weight?: number | null;
bmiStatus: BmiCategory | null; bmiStatus: BmiCategory | null;
smoking?: boolean | null;
}) => [ }) => [
{ {
title: 'dashboard:gender', title: 'dashboard:gender',
description: gender ?? 'dashboard:male', description: gender ?? 'dashboard:male',
icon: <User />, icon: <User />,
iconBg: 'bg-success', iconBg: 'bg-success',
}, },
{ {
title: 'dashboard:age', title: 'dashboard:age',
description: age ? `${age}` : '-', description: age ? `${age}` : '-',
icon: <Clock9 />, icon: <Clock9 />,
iconBg: 'bg-success', iconBg: 'bg-success',
}, },
{ {
title: 'dashboard:height', title: 'dashboard:height',
description: height ? `${height}cm` : '-', description: height ? `${height}cm` : '-',
icon: <RulerHorizontalIcon className="size-4" />, icon: <RulerHorizontalIcon className="size-4" />,
iconBg: 'bg-success', iconBg: 'bg-success',
}, },
{ {
title: 'dashboard:weight', title: 'dashboard:weight',
description: weight ? `${weight}kg` : '-', description: weight ? `${weight}kg` : '-',
icon: <Scale />, icon: <Scale />,
iconBg: 'bg-success', iconBg: 'bg-success',
}, },
{ {
title: 'dashboard:bmi', title: 'dashboard:bmi',
description: bmiFromMetric(weight || 0, height || 0).toString(), description: bmiFromMetric(weight || 0, height || 0).toString(),
icon: <TrendingUp />, icon: <TrendingUp />,
iconBg: getBmiBackgroundColor(bmiStatus), iconBg: getBmiBackgroundColor(bmiStatus),
}, },
{ {
title: 'dashboard:bloodPressure', title: 'dashboard:bloodPressure',
description: '-', description: '-',
icon: <Activity />, icon: <Activity />,
iconBg: 'bg-warning', iconBg: 'bg-warning',
}, },
{ {
title: 'dashboard:cholesterol', title: 'dashboard:cholesterol',
description: '-', description: '-',
icon: <BlendingModeIcon className="size-4" />, icon: <BlendingModeIcon className="size-4" />,
iconBg: 'bg-destructive', iconBg: 'bg-destructive',
}, },
{ {
title: 'dashboard:ldlCholesterol', title: 'dashboard:ldlCholesterol',
description: '-', description: '-',
icon: <Pill />, icon: <Pill />,
iconBg: 'bg-warning', iconBg: 'bg-warning',
}, },
// { // {
// title: 'Score 2', // title: 'Score 2',
// description: 'Normis', // description: 'Normis',
// icon: <LineChart />, // icon: <LineChart />,
// iconBg: 'bg-success', // iconBg: 'bg-success',
// }, // },
{ // {
title: 'dashboard:smoking', // title: 'dashboard:smoking',
description: // description: 'dashboard:respondToQuestion',
isNil(smoking) // descriptionColor: 'text-primary',
? 'dashboard:respondToQuestion' // icon: (
: !!smoking // <Button size="icon" variant="outline" className="px-2 text-black">
? 'common:yes' // <ChevronRight className="size-4 stroke-2" />
: 'common:no', // </Button>
descriptionColor: 'text-primary', // ),
icon: // cardVariant: 'gradient-success' as CardProps['variant'],
isNil(smoking) ? ( // },
<Link href={pathsConfig.app.personalAccountSettings}> ];
<Button size="icon" variant="outline" className="px-2 text-black">
<ChevronRight className="size-4 stroke-2" />
</Button>
</Link>
) : null,
cardVariant: getCardVariant(isNil(smoking) ? null : !smoking),
},
];
const 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({ export default function Dashboard({
account, account,
@@ -148,8 +160,8 @@ export default function Dashboard({
const params = getPersonParameters(account.personal_code!); const params = getPersonParameters(account.personal_code!);
const bmiStatus = getBmiStatus(bmiThresholds, { const bmiStatus = getBmiStatus(bmiThresholds, {
age: params?.age || 0, age: params?.age || 0,
height: account.accountParams?.height || 0, height: account.account_params?.[0]?.height || 0,
weight: account.accountParams?.weight || 0, weight: account.account_params?.[0]?.weight || 0,
}); });
return ( return (
@@ -158,22 +170,21 @@ export default function Dashboard({
{cards({ {cards({
gender: params?.gender, gender: params?.gender,
age: params?.age, age: params?.age,
height: account.accountParams?.height, height: account.account_params?.[0]?.height,
weight: account.accountParams?.weight, weight: account.account_params?.[0]?.weight,
bmiStatus, bmiStatus,
smoking: account.accountParams?.isSmoker,
}).map( }).map(
({ ({
title, title,
description, description,
icon, icon,
iconBg, iconBg,
cardVariant, // cardVariant,
// descriptionColor, // descriptionColor,
}) => ( }) => (
<Card <Card
key={title} key={title}
variant={cardVariant} // variant={cardVariant}
className="flex flex-col justify-between" className="flex flex-col justify-between"
> >
<CardHeader className="items-end-safe"> <CardHeader className="items-end-safe">
@@ -200,7 +211,79 @@ export default function Dashboard({
), ),
)} )}
</div> </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; : 0;
const cartQuantityTotal = const cartQuantityTotal = props.cart?.items?.reduce((acc, item) => acc + item.quantity, 0) ?? 0;
props.cart?.items?.reduce((acc, item) => acc + item.quantity, 0) ?? 0;
const hasCartItems = cartQuantityTotal > 0; const hasCartItems = cartQuantityTotal > 0;
return ( return (
@@ -40,18 +39,15 @@ export async function HomeMenuNavigation(props: {
<div className={`flex items-center ${SIDEBAR_WIDTH_PROPERTY}`}> <div className={`flex items-center ${SIDEBAR_WIDTH_PROPERTY}`}>
<AppLogo href={pathsConfig.app.home} /> <AppLogo href={pathsConfig.app.home} />
</div> </div>
{/* TODO: add search functionality */} <Search
{/* <Search
className="flex grow" className="flex grow"
startElement={<Trans i18nKey="common:search" values={{ end: '...' }} />} startElement={<Trans i18nKey="common:search" values={{ end: '...' }} />}
/> */} />
<div className="flex items-center justify-end gap-3"> <div className="flex items-center justify-end gap-3">
{/* TODO: add wallet functionality
<Card className="px-6 py-2"> <Card className="px-6 py-2">
<span> {Number(0).toFixed(2).replace('.', ',')}</span> <span> {Number(0).toFixed(2).replace('.', ',')}</span>
</Card> </Card>
*/}
{hasCartItems && ( {hasCartItems && (
<Button <Button
className="relative mr-0 h-10 cursor-pointer border-1 px-4 py-2" className="relative mr-0 h-10 cursor-pointer border-1 px-4 py-2"
@@ -60,7 +56,7 @@ export async function HomeMenuNavigation(props: {
<span className="flex items-center text-nowrap">{totalValue}</span> <span className="flex items-center text-nowrap">{totalValue}</span>
</Button> </Button>
)} )}
<Link href={pathsConfig.app.cart}> <Link href="/home/cart">
<Button <Button
variant="ghost" variant="ghost"
className="relative mr-0 h-10 cursor-pointer border-1 px-4 py-2" className="relative mr-0 h-10 cursor-pointer border-1 px-4 py-2"

View File

@@ -4,13 +4,11 @@ import { useMemo } from 'react';
import Link from 'next/link'; import Link from 'next/link';
import SignOutDropdownItem from '@kit/shared/components/sign-out-dropdown-item';
import { StoreCart } from '@medusajs/types'; import { StoreCart } from '@medusajs/types';
import { Cross, 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 { usePersonalAccountData } from '@kit/accounts/hooks/use-personal-account-data';
import { ApplicationRoleEnum } from '@kit/accounts/types/accounts'; import { ApplicationRoleEnum } from '@kit/accounts/types/accounts';
import DropdownLink from '@kit/shared/components/ui/dropdown-link';
import { import {
pathsConfig, pathsConfig,
personalAccountNavigationConfig, personalAccountNavigationConfig,
@@ -93,7 +91,7 @@ export function HomeMobileNavigation(props: {
<If condition={props.cart && hasCartItems}> <If condition={props.cart && hasCartItems}>
<DropdownMenuGroup> <DropdownMenuGroup>
<DropdownLink <DropdownLink
path={pathsConfig.app.cart} path="/home/cart"
label="common:shoppingCartCount" label="common:shoppingCartCount"
Icon={<ShoppingCart className="stroke-[1.5px]" />} Icon={<ShoppingCart className="stroke-[1.5px]" />}
labelOptions={{ count: cartQuantityTotal }} labelOptions={{ count: cartQuantityTotal }}
@@ -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 { cache } from 'react';
import { requireUserInServerComponent } from '@/lib/server/require-user-in-server-component';
import { createAccountsApi } from '@kit/accounts/api'; import { createAccountsApi } from '@kit/accounts/api';
import { getSupabaseServerClient } from '@kit/supabase/server-client'; import { getSupabaseServerClient } from '@kit/supabase/server-client';

View File

@@ -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 { AppBreadcrumbs } from '@kit/ui/app-breadcrumbs';
import { Trans } from '@kit/ui/trans';
import { retrieveCart } from '@lib/data/cart';
import { StoreCart } from '@medusajs/types';
import { UserWorkspaceContextProvider } from '@kit/accounts/components';
import { AppLogo } from '@kit/shared/components/app-logo';
import { pathsConfig } from '@kit/shared/config';
import { Page, PageMobileNavigation, PageNavigation } from '@kit/ui/page';
import { SidebarProvider } from '@kit/ui/shadcn-sidebar';
import { withI18n } from '~/lib/i18n/with-i18n'; import { withI18n } from '~/lib/i18n/with-i18n';
import { SettingsSidebar } from './_components/settings-sidebar';
// home imports
import { HomeMenuNavigation } from '../_components/home-menu-navigation';
import { loadUserWorkspace } from '../_lib/server/load-user-workspace';
import { SettingsMobileNavigation } from './_components/settings-navigation';
function UserSettingsLayout({ children }: React.PropsWithChildren) { // local imports
return <HeaderLayout>{children}</HeaderLayout>; import { HomeLayoutPageHeader } from '../_components/home-page-header';
}
export default withI18n(UserSettingsLayout); function UserSettingsLayout(props: React.PropsWithChildren) {
function HeaderLayout({ children }: React.PropsWithChildren) {
const workspace = use(loadUserWorkspace());
const cart = use(retrieveCart());
return (
<UserWorkspaceContextProvider value={workspace}>
<Page style={'header'}>
<PageNavigation>
<HomeMenuNavigation workspace={workspace} cart={cart} />
</PageNavigation>
<PageMobileNavigation className={'flex items-center justify-between'}>
<MobileNavigation workspace={workspace} cart={cart} />
</PageMobileNavigation>
<SidebarProvider defaultOpen>
<Page style={'sidebar'}>
<PageNavigation>
<SettingsSidebar />
</PageNavigation>
<div className="md:mt-28 min-w-full min-h-full">{children}</div>
</Page>
</SidebarProvider>
</Page>
</UserWorkspaceContextProvider>
);
}
function MobileNavigation({
workspace,
cart,
}: {
workspace: Awaited<ReturnType<typeof loadUserWorkspace>>;
cart: StoreCart | null;
}) {
return ( 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 { PageBody } from '@kit/ui/page';
import { createI18nServerInstance } from '~/lib/i18n/i18n.server'; import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
import { requireUserInServerComponent } from '~/lib/server/require-user-in-server-component';
import { withI18n } from '~/lib/i18n/with-i18n'; import { withI18n } from '~/lib/i18n/with-i18n';
import { loadCurrentUserAccount } from '../_lib/server/load-user-account'; const features = {
import AccountSettingsForm from './_components/account-settings-form'; enableAccountDeletion: featureFlagsConfig.enableAccountDeletion,
import SettingsSectionHeader from './_components/settings-section-header'; enablePasswordUpdate: authConfig.providers.password,
};
const callbackPath = pathsConfig.auth.callback;
const accountHomePath = pathsConfig.app.accountHome;
const paths = {
callback: callbackPath + `?next=${accountHomePath}`,
};
export const generateMetadata = async () => { export const generateMetadata = async () => {
const i18n = await createI18nServerInstance(); const i18n = await createI18nServerInstance();
@@ -16,18 +33,17 @@ export const generateMetadata = async () => {
}; };
}; };
async function PersonalAccountSettingsPage() { function PersonalAccountSettingsPage() {
const account = await loadCurrentUserAccount(); const user = use(requireUserInServerComponent());
return ( return (
<PageBody> <PageBody>
<div className="mx-auto w-full bg-white p-6"> <div className={'flex w-full flex-1 flex-col lg:max-w-2xl'}>
<div className="space-y-6"> <PersonalAccountSettingsContainer
<SettingsSectionHeader userId={user.id}
titleKey="account:accountTabLabel" features={features}
descriptionKey="account:accountTabDescription" paths={paths}
/> />
<AccountSettingsForm account={account} />
</div>
</div> </div>
</PageBody> </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'; 'use client';
import DropdownLink from '@kit/shared/components/ui/dropdown-link'; import Link from 'next/link';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import SignOutDropdownItem from '@kit/shared/components/sign-out-dropdown-item'; import { Home, LogOut, Menu } from 'lucide-react';
import { Home, Menu } from 'lucide-react';
import { AccountSelector } from '@kit/accounts/account-selector'; import { AccountSelector } from '@kit/accounts/account-selector';
import { 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: { function TeamAccountsModal(props: {
accounts: Accounts; accounts: Accounts;

View File

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

View File

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

View File

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

View File

@@ -10,7 +10,6 @@ import {
getClientInstitution, getClientInstitution,
getClientPerson, getClientPerson,
getConfidentiality, getConfidentiality,
getOrderEnteredPerson,
getPais, getPais,
getPatient, getPatient,
getProviderInstitution, getProviderInstitution,
@@ -554,12 +553,14 @@ export async function composeOrderXML({
${getPais(USER, RECIPIENT, orderCreatedAt, orderId)} ${getPais(USER, RECIPIENT, orderCreatedAt, orderId)}
<Tellimus cito="EI"> <Tellimus cito="EI">
<ValisTellimuseId>${orderId}</ValisTellimuseId> <ValisTellimuseId>${orderId}</ValisTellimuseId>
<!--<TellijaAsutus>-->
${getClientInstitution()} ${getClientInstitution()}
<!--<TeostajaAsutus>-->
${getProviderInstitution()} ${getProviderInstitution()}
${getClientPerson()} <!--<TellijaIsik>-->
${getOrderEnteredPerson()} ${getClientPerson(person)}
<TellijaMarkused>${comment ?? ''}</TellijaMarkused> <TellijaMarkused>${comment ?? ''}</TellijaMarkused>
${getPatient(person)} ${getPatient(person)}
${getConfidentiality()} ${getConfidentiality()}
${specimenSection.join('')} ${specimenSection.join('')}
${analysisSection?.join('')} ${analysisSection?.join('')}
@@ -665,7 +666,7 @@ async function syncPrivateMessage({
unit: element.Mootyhik ?? null, unit: element.Mootyhik ?? null,
original_response_element: element, original_response_element: element,
analysis_name: element.UuringNimi || element.KNimetus, analysis_name: element.UuringNimi || element.KNimetus,
comment: element.UuringuKommentaar ?? '', comment: element.UuringuKommentaar
})), })),
); );
} }
@@ -714,7 +715,7 @@ export async function sendOrderToMedipost({
orderedAnalysisElements, orderedAnalysisElements,
}: { }: {
medusaOrderId: string; medusaOrderId: string;
orderedAnalysisElements: { analysisElementId?: number; analysisId?: number }[]; orderedAnalysisElements: { analysisElementId: number }[];
}) { }) {
const medreportOrder = await getOrder({ medusaOrderId }); const medreportOrder = await getOrder({ medusaOrderId });
const account = await getAccountAdmin({ primaryOwnerUserId: medreportOrder.user_id }); const account = await getAccountAdmin({ primaryOwnerUserId: medreportOrder.user_id });
@@ -726,8 +727,8 @@ export async function sendOrderToMedipost({
lastName: account.last_name ?? '', lastName: account.last_name ?? '',
phone: account.phone ?? '', phone: account.phone ?? '',
}, },
orderedAnalysisElementsIds: orderedAnalysisElements.map(({ analysisElementId }) => analysisElementId).filter(Boolean) as number[], orderedAnalysisElementsIds: orderedAnalysisElements.map(({ analysisElementId }) => analysisElementId),
orderedAnalysesIds: orderedAnalysisElements.map(({ analysisId }) => analysisId).filter(Boolean) as number[], orderedAnalysesIds: [],
orderId: medusaOrderId, orderId: medusaOrderId,
orderCreatedAt: new Date(medreportOrder.created_at), orderCreatedAt: new Date(medreportOrder.created_at),
comment: '', comment: '',
@@ -783,13 +784,12 @@ export async function sendOrderToMedipost({
await updateOrderStatus({ medusaOrderId, orderStatus: 'PROCESSING' }); await updateOrderStatus({ medusaOrderId, orderStatus: 'PROCESSING' });
} }
export async function getOrderedAnalysisIds({ export async function getOrderedAnalysisElementsIds({
medusaOrder, medusaOrder,
}: { }: {
medusaOrder: StoreOrder; medusaOrder: StoreOrder;
}): Promise<{ }): Promise<{
analysisElementId?: number; analysisElementId: number;
analysisId?: number;
}[]> { }[]> {
const countryCodes = await listRegions(); const countryCodes = await listRegions();
const countryCode = countryCodes[0]!.countries![0]!.iso_2!; const countryCode = countryCodes[0]!.countries![0]!.iso_2!;
@@ -802,14 +802,6 @@ export async function getOrderedAnalysisIds({
return analysisElements.map(({ id }) => ({ analysisElementId: id })); 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) { async function getOrderedAnalysisPackages(medusaOrder: StoreOrder) {
const orderedPackages = (medusaOrder?.items ?? []).filter(({ product }) => product?.handle.startsWith(ANALYSIS_PACKAGE_HANDLE_PREFIX)); const orderedPackages = (medusaOrder?.items ?? []).filter(({ product }) => product?.handle.startsWith(ANALYSIS_PACKAGE_HANDLE_PREFIX));
const orderedPackageIds = orderedPackages.map(({ product }) => product?.id).filter(Boolean) as string[]; const orderedPackageIds = orderedPackages.map(({ product }) => product?.id).filter(Boolean) as string[];
@@ -849,13 +841,12 @@ export async function getOrderedAnalysisIds({
return analysisElements.map(({ id }) => ({ analysisElementId: id })); return analysisElements.map(({ id }) => ({ analysisElementId: id }));
} }
const [analysisPackageElements, orderedAnalysisElements, orderedAnalyses] = await Promise.all([ const [analysisPackageElements, orderedAnalysisElements] = await Promise.all([
getOrderedAnalysisPackages(medusaOrder), getOrderedAnalysisPackages(medusaOrder),
getOrderedAnalysisElements(medusaOrder), getOrderedAnalysisElements(medusaOrder),
getOrderedAnalyses(medusaOrder),
]); ]);
return [...analysisPackageElements, ...orderedAnalysisElements, ...orderedAnalyses]; return [...analysisPackageElements, ...orderedAnalysisElements];
} }
export async function createMedipostActionLog({ export async function createMedipostActionLog({

View File

@@ -3,7 +3,6 @@
import { import {
getClientInstitution, getClientInstitution,
getClientPerson, getClientPerson,
getOrderEnteredPerson,
getPais, getPais,
getPatient, getPatient,
getProviderInstitution, getProviderInstitution,
@@ -105,8 +104,7 @@ export async function composeOrderTestResponseXML({
<ValisTellimuseId>${orderId}</ValisTellimuseId> <ValisTellimuseId>${orderId}</ValisTellimuseId>
${getClientInstitution({ index: 1 })} ${getClientInstitution({ index: 1 })}
${getProviderInstitution({ index: 1 })} ${getProviderInstitution({ index: 1 })}
${getClientPerson()} ${getClientPerson(person)}
${getOrderEnteredPerson()}
<TellijaMarkused>Siia tuleb tellija poolne märkus</TellijaMarkused> <TellijaMarkused>Siia tuleb tellija poolne märkus</TellijaMarkused>
${getPatient(person)} ${getPatient(person)}

View File

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

View File

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

View File

@@ -21,48 +21,70 @@ export const getPais = (
<Saaja>${recipient}</Saaja> <Saaja>${recipient}</Saaja>
<Aeg>${format(createdAt, DATE_TIME_FORMAT)}</Aeg> <Aeg>${format(createdAt, DATE_TIME_FORMAT)}</Aeg>
<SaadetisId>${orderId}</SaadetisId> <SaadetisId>${orderId}</SaadetisId>
<Email>info@medreport.ee</Email> <Email>argo@medreport.ee</Email>
</Pais>`; </Pais>`;
}; };
export const getClientInstitution = ({ index }: { index?: number } = {}) => { export const getClientInstitution = ({ index }: { index?: number } = {}) => {
if (isProd) {
// return correct data
}
return `<Asutus tyyp="TELLIJA" ${index ? ` jarjenumber="${index}"` : ''}> return `<Asutus tyyp="TELLIJA" ${index ? ` jarjenumber="${index}"` : ''}>
<AsutuseId>16381793</AsutuseId> <AsutuseId>16381793</AsutuseId>
<AsutuseNimi>MedReport OÜ</AsutuseNimi> <AsutuseNimi>MedReport OÜ</AsutuseNimi>
<AsutuseKood>MRP</AsutuseKood> <AsutuseKood>TSU</AsutuseKood>
<Telefon>+37258871517</Telefon> <Telefon>+37258871517</Telefon>
</Asutus>`; </Asutus>`;
}; };
export const getProviderInstitution = ({ index }: { index?: number } = {}) => { export const getProviderInstitution = ({ index }: { index?: number } = {}) => {
if (isProd) {
// return correct data
}
return `<Asutus tyyp="TEOSTAJA" ${index ? ` jarjenumber="${index}"` : ''}> return `<Asutus tyyp="TEOSTAJA" ${index ? ` jarjenumber="${index}"` : ''}>
<AsutuseId>11107913</AsutuseId> <AsutuseId>11107913</AsutuseId>
<AsutuseNimi>Synlab Eesti OÜ</AsutuseNimi> <AsutuseNimi>Synlab HTI Tallinn</AsutuseNimi>
<AsutuseKood>HTI</AsutuseKood> <AsutuseKood>SLA</AsutuseKood>
<AllyksuseNimi>Synlab HTI Tallinn</AllyksuseNimi> <AllyksuseNimi>Synlab HTI Tallinn</AllyksuseNimi>
<Telefon>+37217123</Telefon> <Telefon>+3723417123</Telefon>
</Asutus>`; </Asutus>`;
}; };
export const getClientPerson = () => { export const getClientPerson = ({
idCode,
firstName,
lastName,
phone,
}: {
idCode: string,
firstName: string,
lastName: string,
phone: string,
}) => {
if (isProd) {
// return correct data
}
return `<Personal tyyp="TELLIJA" jarjenumber="1"> return `<Personal tyyp="TELLIJA" jarjenumber="1">
<PersonalOID>1.3.6.1.4.1.28284.6.2.4.9</PersonalOID> <PersonalOID>1.3.6.1.4.1.28284.6.2.4.9</PersonalOID>
<PersonalKood>D07907</PersonalKood> <PersonalKood>${idCode}</PersonalKood>
<PersonalPerekonnaNimi>Eduard</PersonalPerekonnaNimi> <PersonalPerekonnaNimi>${lastName}</PersonalPerekonnaNimi>
<PersonalEesNimi>Tsvetkov</PersonalEesNimi> <PersonalEesNimi>${firstName}</PersonalEesNimi>
<Telefon>+37258131202</Telefon> ${phone ? `<Telefon>${phone.startsWith('+372') ? phone : `+372${phone}`}</Telefon>` : ''}
</Personal>`; </Personal>`;
}; };
export const getOrderEnteredPerson = () => { // export const getOrderEnteredPerson = () => {
return `<Personal tyyp="SISESTAJA" jarjenumber="2"> // if (isProd) {
<PersonalOID>1.3.6.1.4.1.28284.6.2.4.9</PersonalOID> // // return correct data
<PersonalKood>D07907</PersonalKood> // }
<PersonalPerekonnaNimi>Eduard</PersonalPerekonnaNimi> // return `<Personal tyyp="SISESTAJA" jarjenumber="1">
<PersonalEesNimi>Tsvetkov</PersonalEesNimi> // <PersonalOID>1.3.6.1.4.1.28284.6.2.4.9</PersonalOID>
<Telefon>+37258131202</Telefon> // <PersonalKood>D07907</PersonalKood>
</Personal>`; // <PersonalPerekonnaNimi>Eduard</PersonalPerekonnaNimi>
}; // <PersonalEesNimi>Tsvetkov</PersonalEesNimi>
// <Telefon>+37258131202</Telefon>
// </Personal>`;
// };
export const getPatient = ({ export const getPatient = ({
idCode, idCode,

View File

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

View File

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

View File

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

View File

@@ -21,25 +21,39 @@ export const PaymentTypeSchema = z.enum(['one-time', 'recurring']);
export const LineItemSchema = z export const LineItemSchema = z
.object({ .object({
id: z id: z
.string() .string({
.describe('Unique identifier for the line item. Defined by the Provider.') description:
'Unique identifier for the line item. Defined by the Provider.',
})
.min(1), .min(1),
name: z 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), .min(1),
description: z description: z
.string().describe('Description of the line item. Displayed to the user and will replace the auto-generated description inferred' + .string({
' from the line item. This is useful if you want to provide a more detailed description to the user.') 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(), .optional(),
cost: z 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), .min(0),
type: LineItemTypeSchema, type: LineItemTypeSchema,
unit: z 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(), .optional(),
setupFee: z 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() .positive()
.optional(), .optional(),
tiers: z tiers: z
@@ -78,10 +92,14 @@ export const LineItemSchema = z
export const PlanSchema = z export const PlanSchema = z
.object({ .object({
id: z id: z
.string().describe('Unique identifier for the plan. Defined by yourself.') .string({
description: 'Unique identifier for the plan. Defined by yourself.',
})
.min(1), .min(1),
name: z name: z
.string().describe('Name of the plan. Displayed to the user.') .string({
description: 'Name of the plan. Displayed to the user.',
})
.min(1), .min(1),
interval: BillingIntervalSchema.optional(), interval: BillingIntervalSchema.optional(),
custom: z.boolean().default(false).optional(), custom: z.boolean().default(false).optional(),
@@ -106,7 +124,10 @@ export const PlanSchema = z
}, },
), ),
trialDays: 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() .positive()
.optional(), .optional(),
paymentType: PaymentTypeSchema, paymentType: PaymentTypeSchema,
@@ -188,34 +209,54 @@ export const PlanSchema = z
const ProductSchema = z const ProductSchema = z
.object({ .object({
id: z 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), .min(1),
name: z name: z
.string().describe('Name of the product. Displayed to the user.') .string({
description: 'Name of the product. Displayed to the user.',
})
.min(1), .min(1),
description: z description: z
.string().describe('Description of the product. Displayed to the user.') .string({
description: 'Description of the product. Displayed to the user.',
})
.min(1), .min(1),
currency: z 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) .min(3)
.max(3), .max(3),
badge: z 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(), .optional(),
features: z features: z
.array( .array(
z.string(), z.string({
).describe('Features of the product. Displayed to the user.') description: 'Features of the product. Displayed to the user.',
}),
)
.nonempty(), .nonempty(),
enableDiscountField: z 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(), .optional(),
highlighted: z highlighted: z
.boolean().describe('Highlight this product. Displayed to the user.') .boolean({
description: 'Highlight this product. Displayed to the user.',
})
.optional(), .optional(),
hidden: z hidden: z
.boolean().describe('Hide this product from being displayed to users.') .boolean({
description: 'Hide this product from being displayed to users.',
})
.optional(), .optional(),
plans: z.array(PlanSchema), plans: z.array(PlanSchema),
}) })

View File

@@ -1,10 +1,14 @@
import { z } from 'zod'; import { z } from 'zod';
export const ReportBillingUsageSchema = z.object({ 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 eventName: z
.string() .string({
.describe('The name of the event that triggered the usage') description: 'The name of the event that triggered the usage',
})
.optional(), .optional(),
usage: z.object({ usage: z.object({
quantity: z.number(), quantity: z.number(),

View File

@@ -3,8 +3,8 @@ import { z } from 'zod';
export const UpdateHealthBenefitSchema = z.object({ export const UpdateHealthBenefitSchema = z.object({
occurance: z occurance: z
.string({ .string({
error: 'Occurance is required', required_error: 'Occurance is required',
}) })
.nonempty(), .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({ .object({
secretKey: z secretKey: z
.string({ .string({
error: `Please provide the variable MONTONIO_SECRET_KEY`, required_error: `Please provide the variable MONTONIO_SECRET_KEY`,
}) })
.min(1), .min(1),
apiUrl: z apiUrl: z
.string({ .string({
error: `Please provide the variable MONTONIO_API_URL`, required_error: `Please provide the variable MONTONIO_API_URL`,
}) })
.min(1), .min(1),
}); });

View File

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

View File

@@ -4,9 +4,9 @@ import { DatabaseWebhookVerifierService } from './database-webhook-verifier.serv
const webhooksSecret = z const webhooksSecret = z
.string({ .string({
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) .min(1)
.parse(process.env.SUPABASE_DB_WEBHOOK_SECRET); .parse(process.env.SUPABASE_DB_WEBHOOK_SECRET);

View File

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

View File

@@ -101,14 +101,14 @@ export function PersonalAccountDropdown({
personalAccountData?.application_role === ApplicationRoleEnum.SuperAdmin; personalAccountData?.application_role === ApplicationRoleEnum.SuperAdmin;
return hasAdminRole && hasTotpFactor; return hasAdminRole && hasTotpFactor;
}, [personalAccountData, hasTotpFactor]); }, [user, personalAccountData, hasTotpFactor]);
const isDoctor = useMemo(() => { const isDoctor = useMemo(() => {
const hasDoctorRole = const hasDoctorRole =
personalAccountData?.application_role === ApplicationRoleEnum.Doctor; personalAccountData?.application_role === ApplicationRoleEnum.Doctor;
return hasDoctorRole && hasTotpFactor; return hasDoctorRole && hasTotpFactor;
}, [personalAccountData, hasTotpFactor]); }, [user, personalAccountData, hasTotpFactor]);
return ( return (
<DropdownMenu> <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 { useFetchAuthFactors } from '@kit/supabase/hooks/use-fetch-mfa-factors';
import { useSupabase } from '@kit/supabase/hooks/use-supabase'; import { useSupabase } from '@kit/supabase/hooks/use-supabase';
import { useUser } from '@kit/supabase/hooks/use-user';
import { useFactorsMutationKey } from '@kit/supabase/hooks/use-user-factors-mutation-key'; import { useFactorsMutationKey } from '@kit/supabase/hooks/use-user-factors-mutation-key';
import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert'; import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert';
import { import {
@@ -47,18 +46,13 @@ import { Trans } from '@kit/ui/trans';
import { MultiFactorAuthSetupDialog } from './multi-factor-auth-setup-dialog'; import { MultiFactorAuthSetupDialog } from './multi-factor-auth-setup-dialog';
export function MultiFactorAuthFactorsList() { export function MultiFactorAuthFactorsList(props: { userId: string }) {
const { data: user } = useUser();
if (!user?.id) {
return null;
}
return ( return (
<div className={'flex flex-col space-y-4'}> <div className={'flex flex-col space-y-4'}>
<FactorsTableContainer userId={user?.id} /> <FactorsTableContainer userId={props.userId} />
<div> <div>
<MultiFactorAuthSetupDialog userId={user?.id} /> <MultiFactorAuthSetupDialog userId={props.userId} />
</div> </div>
</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 { Database } from '@kit/supabase/database';
import { AnalysisResultDetails, UserAnalysis } from '../types/accounts'; import {
AnalysisResultDetails,
UserAnalysis,
} from '../types/accounts';
export type AccountWithParams = export type AccountWithParams =
Database['medreport']['Tables']['accounts']['Row'] & { Database['medreport']['Tables']['accounts']['Row'] & {
accountParams: account_params:
| (Pick< | Pick<
Database['medreport']['Tables']['account_params']['Row'], Database['medreport']['Tables']['account_params']['Row'],
'weight' | 'height' 'weight' | 'height'
> & { >[]
isSmoker:
| Database['medreport']['Tables']['account_params']['Row']['is_smoker']
| null;
})
| null; | null;
}; };
@@ -35,9 +34,7 @@ class AccountsApi {
const { data, error } = await this.client const { data, error } = await this.client
.schema('medreport') .schema('medreport')
.from('accounts') .from('accounts')
.select( .select('*, account_params: account_params (weight, height)')
'*, accountParams: account_params (weight, height, isSmoker:is_smoker)',
)
.eq('id', id) .eq('id', id)
.single(); .single();

View File

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

View File

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

View File

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

View File

@@ -87,10 +87,7 @@ export async function getOrSetCart(countryCode: string) {
return cart; return cart;
} }
export async function updateCart( export async function updateCart({ id, ...data }: HttpTypes.StoreUpdateCart & { id?: string }) {
{ id, ...data }: HttpTypes.StoreUpdateCart & { id?: string },
{ onSuccess, onError }: { onSuccess: () => void, onError: () => void } = { onSuccess: () => {}, onError: () => {} },
) {
const cartId = id || (await getCartId()); const cartId = id || (await getCartId());
if (!cartId) { if (!cartId) {
@@ -112,13 +109,9 @@ export async function updateCart(
const fulfillmentCacheTag = await getCacheTag("fulfillment"); const fulfillmentCacheTag = await getCacheTag("fulfillment");
revalidateTag(fulfillmentCacheTag); revalidateTag(fulfillmentCacheTag);
onSuccess();
return cart; return cart;
}) })
.catch((e) => { .catch(medusaError);
onError();
return medusaError(e);
});
} }
export async function addToCart({ export async function addToCart({
@@ -266,10 +259,7 @@ export async function initiatePaymentSession(
.catch(medusaError); .catch(medusaError);
} }
export async function applyPromotions( export async function applyPromotions(codes: string[]) {
codes: string[],
{ onSuccess, onError }: { onSuccess: () => void, onError: () => void } = { onSuccess: () => {}, onError: () => {} },
) {
const cartId = await getCartId(); const cartId = await getCartId();
if (!cartId) { if (!cartId) {
@@ -288,13 +278,8 @@ export async function applyPromotions(
const fulfillmentCacheTag = await getCacheTag("fulfillment"); const fulfillmentCacheTag = await getCacheTag("fulfillment");
revalidateTag(fulfillmentCacheTag); revalidateTag(fulfillmentCacheTag);
onSuccess();
}) })
.catch((e) => { .catch(medusaError);
onError();
return medusaError(e);
});
} }
export async function applyGiftCard(code: string) { export async function applyGiftCard(code: string) {
@@ -442,7 +427,7 @@ export async function placeOrder(cartId?: string, options: { revalidateCacheTags
} else { } else {
throw new Error("Cart is not an order"); throw new Error("Cart is not an order");
} }
return retrieveOrder(cartRes.order.id); return retrieveOrder(cartRes.order.id);
} }

View File

@@ -127,7 +127,7 @@ export async function login(_currentState: unknown, formData: FormData) {
} }
} }
export async function signout(countryCode?: string, shouldRedirect = true) { export async function signout(countryCode: string) {
await sdk.auth.logout() await sdk.auth.logout()
await removeAuthToken() await removeAuthToken()
@@ -140,9 +140,7 @@ export async function signout(countryCode?: string, shouldRedirect = true) {
const cartCacheTag = await getCacheTag("carts") const cartCacheTag = await getCacheTag("carts")
revalidateTag(cartCacheTag) revalidateTag(cartCacheTag)
if (shouldRedirect) { redirect(`/${countryCode}/account`)
redirect(`/${countryCode!}/account`)
}
} }
export async function transferCart() { export async function transferCart() {
@@ -274,12 +272,7 @@ export async function medusaLoginOrRegister(credentials: {
password, password,
}); });
await setAuthToken(token as string); await setAuthToken(token as string);
await transferCart();
try {
await transferCart();
} catch (e) {
console.error("Failed to transfer cart", e);
}
const customerCacheTag = await getCacheTag("customers"); const customerCacheTag = await getCacheTag("customers");
revalidateTag(customerCacheTag); revalidateTag(customerCacheTag);
@@ -314,12 +307,7 @@ export async function medusaLoginOrRegister(credentials: {
const customerCacheTag = await getCacheTag("customers"); const customerCacheTag = await getCacheTag("customers");
revalidateTag(customerCacheTag); revalidateTag(customerCacheTag);
await transferCart();
try {
await transferCart();
} catch (e) {
console.error("Failed to transfer cart", e);
}
const customer = await retrieveCustomer(); const customer = await retrieveCustomer();
if (!customer) { if (!customer) {

View File

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

View File

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

View File

@@ -8,9 +8,9 @@ type Config = z.infer<typeof MailerSchema>;
const RESEND_API_KEY = z const RESEND_API_KEY = z
.string({ .string({
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); .parse(process.env.RESEND_API_KEY);
export function createResendMailer() { export function createResendMailer() {

View File

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

View File

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

View File

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

View File

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

View File

@@ -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({ .object({
name: z name: z
.string({ .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), .min(1),
title: z title: z
.string({ .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), .min(1),
description: z.string({ description: z.string({
error: `Please provide the variable NEXT_PUBLIC_SITE_DESCRIPTION`, description: `This is the default description of your SaaS.`,
}) required_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'`
}), }),
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 locale: z
.string({ .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'), .default('en'),
theme: z.enum(['light', 'dark', 'system']), theme: z.enum(['light', 'dark', 'system']),
production: z.boolean(), production: z.boolean(),

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -7,14 +7,14 @@ export function getSupabaseClientKeys() {
return z return z
.object({ .object({
url: z.string({ url: z.string({
error: `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`,
.describe(`This is the URL of your hosted Supabase instance. Please provide the variable NEXT_PUBLIC_SUPABASE_URL.`), }),
anonKey: z anonKey: z
.string({ .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), .min(1),
}) })
.parse({ .parse({

View File

@@ -1,14 +1,12 @@
import { useMutation } from '@tanstack/react-query'; import { useMutation } from '@tanstack/react-query';
import { useSupabase } from './use-supabase'; import { useSupabase } from './use-supabase';
import { signout } from '../../../features/medusa-storefront/src/lib/data/customer';
export function useSignOut() { export function useSignOut() {
const client = useSupabase(); const client = useSupabase();
return useMutation({ return useMutation({
mutationFn: async () => { mutationFn: () => {
await signout(undefined, false);
return client.auth.signOut(); return client.auth.signOut();
}, },
}); });

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 // this is most likely a session error or the user is not logged in
if (response.error) { if (response.error) {
return null; return undefined;
} }
if (response.data?.user) { if (response.data?.user) {

View File

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

View File

@@ -5,7 +5,6 @@
"emptyCartMessageDescription": "Add items to your cart to continue.", "emptyCartMessageDescription": "Add items to your cart to continue.",
"subtotal": "Subtotal", "subtotal": "Subtotal",
"total": "Total", "total": "Total",
"promotionsTotal": "Promotions total",
"table": { "table": {
"item": "Item", "item": "Item",
"quantity": "Quantity", "quantity": "Quantity",
@@ -25,13 +24,10 @@
"timeoutAction": "Continue" "timeoutAction": "Continue"
}, },
"discountCode": { "discountCode": {
"title": "Gift card or promotion code",
"label": "Add Promotion Code(s)", "label": "Add Promotion Code(s)",
"apply": "Apply", "apply": "Apply",
"subtitle": "If you wish, you can add a promotion code", "subtitle": "If you wish, you can add a promotion code",
"placeholder": "Enter promotion code", "placeholder": "Enter promotion code"
"remove": "Remove promotion code",
"appliedCodes": "Promotion(s) applied:"
}, },
"items": { "items": {
"synlabAnalyses": { "synlabAnalyses": {

View File

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

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

View File

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

View File

@@ -4,7 +4,6 @@
"emptyCartMessage": "Sinu ostukorv on tühi", "emptyCartMessage": "Sinu ostukorv on tühi",
"emptyCartMessageDescription": "Lisa tooteid ostukorvi, et jätkata.", "emptyCartMessageDescription": "Lisa tooteid ostukorvi, et jätkata.",
"subtotal": "Vahesumma", "subtotal": "Vahesumma",
"promotionsTotal": "Soodustuse summa",
"total": "Summa", "total": "Summa",
"table": { "table": {
"item": "Toode", "item": "Toode",
@@ -29,13 +28,7 @@
"label": "Lisa promo kood", "label": "Lisa promo kood",
"apply": "Rakenda", "apply": "Rakenda",
"subtitle": "Kui soovid, võid lisada promo koodi", "subtitle": "Kui soovid, võid lisada promo koodi",
"placeholder": "Sisesta promo kood", "placeholder": "Sisesta promo kood"
"remove": "Eemalda promo kood",
"appliedCodes": "Rakendatud sooduskoodid:",
"removeError": "Sooduskoodi eemaldamine ebaõnnestus",
"removeSuccess": "Sooduskood eemaldatud",
"addError": "Sooduskoodi rakendamine ebaõnnestus",
"addSuccess": "Sooduskood rakendatud"
}, },
"items": { "items": {
"synlabAnalyses": { "synlabAnalyses": {

View File

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

Some files were not shown because too many files have changed in this diff Show More