Merge branch 'develop' of https://github.com/MR-medreport/MRB2B into MED-103
This commit is contained in:
@@ -6,10 +6,10 @@
|
||||
## PUBLIC KEYS OR CONFIGURATION ARE OKAY TO BE PLACED HERE.
|
||||
|
||||
# SUPABASE
|
||||
NEXT_PUBLIC_SUPABASE_URL=https://oqsdacktkhmbylmzstjq.supabase.co
|
||||
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Im9xc2RhY2t0a2htYnlsbXpzdGpxIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NDY1MjgxMjMsImV4cCI6MjA2MjEwNDEyM30.LdHCTWxijFmhXdnT9KVuLRAVbtSwY7OO-oLtpd8GmO0
|
||||
# NEXT_PUBLIC_SUPABASE_URL=https://oqsdacktkhmbylmzstjq.supabase.co
|
||||
# NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Im9xc2RhY2t0a2htYnlsbXpzdGpxIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NDY1MjgxMjMsImV4cCI6MjA2MjEwNDEyM30.LdHCTWxijFmhXdnT9KVuLRAVbtSwY7OO-oLtpd8GmO0
|
||||
|
||||
NEXT_PUBLIC_SITE_URL=https://test.medreport.ee
|
||||
# NEXT_PUBLIC_SITE_URL=https://test.medreport.ee
|
||||
|
||||
# MONTONIO
|
||||
NEXT_PUBLIC_MONTONIO_ACCESS_KEY=7da5d7fa-3383-4997-9435-46aa818f4ead
|
||||
# # MONTONIO
|
||||
# NEXT_PUBLIC_MONTONIO_ACCESS_KEY=7da5d7fa-3383-4997-9435-46aa818f4ead
|
||||
|
||||
15
.env.staging
Normal file
15
.env.staging
Normal file
@@ -0,0 +1,15 @@
|
||||
# PRODUCTION ENVIRONMENT VARIABLES
|
||||
|
||||
## DO NOT ADD VARS HERE UNLESS THEY ARE PUBLIC OR NOT SENSITIVE
|
||||
## THIS ENV IS USED FOR PRODUCTION AND IS COMMITED TO THE REPO
|
||||
## AVOID PLACING SENSITIVE DATA IN THIS FILE.
|
||||
## PUBLIC KEYS OR CONFIGURATION ARE OKAY TO BE PLACED HERE.
|
||||
|
||||
# SUPABASE
|
||||
# NEXT_PUBLIC_SUPABASE_URL=https://klocrucggryikaxzvxgc.supabase.co
|
||||
# NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Imtsb2NydWNnZ3J5aWtheHp2eGdjIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTY5ODQ2MjgsImV4cCI6MjA3MjU2MDYyOH0.2XOQngowcymiSUZO_XEEWAWzco2uRIjwG7TAeRRLIdU
|
||||
|
||||
# NEXT_PUBLIC_SITE_URL=https://test.medreport.ee
|
||||
|
||||
# # MONTONIO
|
||||
# NEXT_PUBLIC_MONTONIO_ACCESS_KEY=7da5d7fa-3383-4997-9435-46aa818f4ead
|
||||
25
Dockerfile
25
Dockerfile
@@ -11,6 +11,7 @@ COPY packages packages
|
||||
COPY tooling tooling
|
||||
COPY .env .env
|
||||
COPY .env.production .env.production
|
||||
COPY .env.staging .env.staging
|
||||
|
||||
# Load env file and echo a specific variable
|
||||
# RUN dotenv -e .env -- printenv | grep 'SUPABASE' || true
|
||||
@@ -20,33 +21,33 @@ COPY . .
|
||||
|
||||
ENV NODE_ENV=production
|
||||
|
||||
# 🔍 Optional: Log key envs for debug
|
||||
RUN echo "📄 .env.production contents:" && cat .env.production \
|
||||
&& echo "🔧 Current ENV available to Next.js build:" && printenv | grep -E 'SUPABASE|STRIPE|NEXT|NODE_ENV' || true
|
||||
|
||||
RUN set -a \
|
||||
&& . .env \
|
||||
&& . .env.production \
|
||||
&& set +a \
|
||||
&& node check-env.js \
|
||||
&& pnpm build
|
||||
&& . .env \
|
||||
&& . .env.production \
|
||||
&& . .env.staging \
|
||||
&& set +a \
|
||||
&& node check-env.js \
|
||||
&& pnpm build
|
||||
|
||||
|
||||
# --- Stage 2: Runtime ---
|
||||
FROM node:20-alpine
|
||||
ARG APP_ENV=production
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY --from=builder /app ./
|
||||
|
||||
RUN cp ".env.${APP_ENV}" .env.local
|
||||
|
||||
RUN npm install -g pnpm@9 \
|
||||
&& pnpm install --prod --frozen-lockfile
|
||||
&& pnpm install --prod --frozen-lockfile
|
||||
|
||||
ENV NODE_ENV=production
|
||||
|
||||
# 🔍 Optional: Log key envs for debug
|
||||
RUN echo "📄 .env.production contents:" && cat .env.production \
|
||||
&& echo "🔧 Current ENV available to Next.js build:" && printenv | grep -E 'SUPABASE|STRIPE|NEXT|NODE_ENV' || true
|
||||
RUN echo "📄 .env contents:" && cat .env.local \
|
||||
&& echo "🔧 Current ENV available to Next.js build:" && printenv | grep -E 'SUPABASE|STRIPE|NEXT|NODE_ENV' || true
|
||||
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
@@ -9,18 +9,16 @@ import { ContactEmailSchema } from '../contact-email.schema';
|
||||
|
||||
const contactEmail = z
|
||||
.string({
|
||||
description: `The email where you want to receive the contact form submissions.`,
|
||||
required_error:
|
||||
error:
|
||||
'Contact email is required. Please use the environment variable CONTACT_EMAIL.',
|
||||
})
|
||||
}).describe(`The email where you want to receive the contact form submissions.`)
|
||||
.parse(process.env.CONTACT_EMAIL);
|
||||
|
||||
const emailFrom = z
|
||||
.string({
|
||||
description: `The email sending address.`,
|
||||
required_error:
|
||||
error:
|
||||
'Sender email is required. Please use the environment variable EMAIL_SENDER.',
|
||||
})
|
||||
}).describe(`The email sending address.`)
|
||||
.parse(process.env.EMAIL_SENDER);
|
||||
|
||||
export const sendContactEmail = enhanceAction(
|
||||
|
||||
@@ -164,10 +164,15 @@ export default async function syncAnalysisGroups() {
|
||||
console.info('Inserting sync entry');
|
||||
await createSyncSuccessEntry();
|
||||
} catch (e) {
|
||||
await createSyncFailEntry(JSON.stringify(e));
|
||||
console.error(e);
|
||||
const errorMessage = e instanceof Error ? e.message : String(e);
|
||||
await createSyncFailEntry(JSON.stringify({
|
||||
message: errorMessage,
|
||||
stack: e instanceof Error ? e.stack : undefined,
|
||||
name: e instanceof Error ? e.name : 'Unknown',
|
||||
}, null, 2));
|
||||
console.error('Sync failed:', e);
|
||||
throw new Error(
|
||||
`Failed to sync public message data, error: ${JSON.stringify(e)}`,
|
||||
`Failed to sync public message data, error: ${errorMessage}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import loadEnv from "../handler/load-env";
|
||||
import validateApiKey from "../handler/validate-api-key";
|
||||
import { getOrderedAnalysisElementsIds, sendOrderToMedipost } from "~/lib/services/medipost.service";
|
||||
import { getOrderedAnalysisIds, sendOrderToMedipost } from "~/lib/services/medipost.service";
|
||||
import { retrieveOrder } from "@lib/data/orders";
|
||||
import { getMedipostDispatchTries } from "~/lib/services/audit.service";
|
||||
|
||||
@@ -25,7 +25,7 @@ export const POST = async (request: NextRequest) => {
|
||||
|
||||
try {
|
||||
const medusaOrder = await retrieveOrder(medusaOrderId);
|
||||
const orderedAnalysisElements = await getOrderedAnalysisElementsIds({ medusaOrder });
|
||||
const orderedAnalysisElements = await getOrderedAnalysisIds({ medusaOrder });
|
||||
await sendOrderToMedipost({ medusaOrderId, orderedAnalysisElements });
|
||||
console.info("Successfully sent order to medipost");
|
||||
return NextResponse.json({
|
||||
|
||||
@@ -3,7 +3,7 @@ import { getAnalysisOrdersAdmin } from "~/lib/services/order.service";
|
||||
import { composeOrderTestResponseXML, sendPrivateMessageTestResponse } from "~/lib/services/medipostTest.service";
|
||||
import { retrieveOrder } from "@lib/data";
|
||||
import { getAccountAdmin } from "~/lib/services/account.service";
|
||||
import { getOrderedAnalysisElementsIds } from "~/lib/services/medipost.service";
|
||||
import { getOrderedAnalysisIds } from "~/lib/services/medipost.service";
|
||||
import loadEnv from "../handler/load-env";
|
||||
import validateApiKey from "../handler/validate-api-key";
|
||||
|
||||
@@ -24,7 +24,7 @@ export async function POST(request: NextRequest) {
|
||||
const medusaOrder = await retrieveOrder(medusaOrderId)
|
||||
|
||||
const account = await getAccountAdmin({ primaryOwnerUserId: medreportOrder.user_id });
|
||||
const orderedAnalysisElementsIds = await getOrderedAnalysisElementsIds({ medusaOrder });
|
||||
const orderedAnalysisElementsIds = await getOrderedAnalysisIds({ medusaOrder });
|
||||
|
||||
console.info(`Sending test response for order=${medusaOrderId} with ${orderedAnalysisElementsIds.length} ordered analysis elements`);
|
||||
const idsToSend = orderedAnalysisElementsIds;
|
||||
@@ -35,8 +35,8 @@ export async function POST(request: NextRequest) {
|
||||
lastName: account.last_name ?? '',
|
||||
phone: account.phone ?? '',
|
||||
},
|
||||
orderedAnalysisElementsIds: idsToSend.map(({ analysisElementId }) => analysisElementId),
|
||||
orderedAnalysesIds: [],
|
||||
orderedAnalysisElementsIds: idsToSend.map(({ analysisElementId }) => analysisElementId).filter(Boolean) as number[],
|
||||
orderedAnalysesIds: idsToSend.map(({ analysisId }) => analysisId).filter(Boolean) as number[],
|
||||
orderId: medusaOrderId,
|
||||
orderCreatedAt: new Date(medreportOrder.created_at),
|
||||
});
|
||||
|
||||
@@ -3,7 +3,7 @@ import { getOrder } from "~/lib/services/order.service";
|
||||
import { composeOrderTestResponseXML, sendPrivateMessageTestResponse } from "~/lib/services/medipostTest.service";
|
||||
import { retrieveOrder } from "@lib/data";
|
||||
import { getAccountAdmin } from "~/lib/services/account.service";
|
||||
import { createMedipostActionLog, getOrderedAnalysisElementsIds } from "~/lib/services/medipost.service";
|
||||
import { createMedipostActionLog, getOrderedAnalysisIds } from "~/lib/services/medipost.service";
|
||||
|
||||
export async function POST(request: Request) {
|
||||
// const isDev = process.env.NODE_ENV === 'development';
|
||||
@@ -11,16 +11,15 @@ export async function POST(request: Request) {
|
||||
// return NextResponse.json({ error: 'This endpoint is only available in development mode' }, { status: 403 });
|
||||
// }
|
||||
|
||||
const { medusaOrderId, maxItems = null } = await request.json();
|
||||
const { medusaOrderId } = await request.json();
|
||||
|
||||
const medusaOrder = await retrieveOrder(medusaOrderId)
|
||||
const medreportOrder = await getOrder({ medusaOrderId });
|
||||
|
||||
const account = await getAccountAdmin({ primaryOwnerUserId: medreportOrder.user_id });
|
||||
const orderedAnalysisElementsIds = await getOrderedAnalysisElementsIds({ medusaOrder });
|
||||
const orderedAnalysisElementsIds = await getOrderedAnalysisIds({ medusaOrder });
|
||||
|
||||
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;
|
||||
console.info(`Sending test response for order=${medusaOrderId} with ${orderedAnalysisElementsIds.length} ordered analysis elements`);
|
||||
const messageXml = await composeOrderTestResponseXML({
|
||||
person: {
|
||||
idCode: account.personal_code!,
|
||||
@@ -28,8 +27,8 @@ export async function POST(request: Request) {
|
||||
lastName: account.last_name ?? '',
|
||||
phone: account.phone ?? '',
|
||||
},
|
||||
orderedAnalysisElementsIds: idsToSend.map(({ analysisElementId }) => analysisElementId),
|
||||
orderedAnalysesIds: [],
|
||||
orderedAnalysisElementsIds: orderedAnalysisElementsIds.map(({ analysisElementId }) => analysisElementId).filter(Boolean) as number[],
|
||||
orderedAnalysesIds: orderedAnalysisElementsIds.map(({ analysisId }) => analysisId).filter(Boolean) as number[],
|
||||
orderId: medusaOrderId,
|
||||
orderCreatedAt: new Date(medreportOrder.created_at),
|
||||
});
|
||||
|
||||
@@ -3,17 +3,17 @@ import { z } from 'zod';
|
||||
export const UpdateAccountSchema = z.object({
|
||||
firstName: z
|
||||
.string({
|
||||
required_error: 'First name is required',
|
||||
error: 'First name is required',
|
||||
})
|
||||
.nonempty(),
|
||||
lastName: z
|
||||
.string({
|
||||
required_error: 'Last name is required',
|
||||
error: 'Last name is required',
|
||||
})
|
||||
.nonempty(),
|
||||
personalCode: z
|
||||
.string({
|
||||
required_error: 'Personal code is required',
|
||||
error: 'Personal code is required',
|
||||
})
|
||||
.nonempty(),
|
||||
email: z.string().email({
|
||||
@@ -21,21 +21,25 @@ export const UpdateAccountSchema = z.object({
|
||||
}),
|
||||
phone: z
|
||||
.string({
|
||||
required_error: 'Phone number is required',
|
||||
error: 'Phone number is required',
|
||||
})
|
||||
.nonempty(),
|
||||
city: z.string().optional(),
|
||||
weight: z
|
||||
.number({
|
||||
required_error: 'Weight is required',
|
||||
invalid_type_error: 'Weight must be a number',
|
||||
error: (issue) =>
|
||||
issue.input === undefined
|
||||
? 'Weight is required'
|
||||
: 'Weight must be a number',
|
||||
})
|
||||
.gt(0, { message: 'Weight must be greater than 0' }),
|
||||
|
||||
height: z
|
||||
.number({
|
||||
required_error: 'Height is required',
|
||||
invalid_type_error: 'Height must be a number',
|
||||
error: (issue) =>
|
||||
issue.input === undefined
|
||||
? 'Height is required'
|
||||
: 'Height must be a number',
|
||||
})
|
||||
.gt(0, { message: 'Height must be greater than 0' }),
|
||||
userConsent: z.boolean().refine((val) => val === true, {
|
||||
|
||||
@@ -8,7 +8,7 @@ import { listProductTypes } from "@lib/data/products";
|
||||
import { placeOrder, retrieveCart } from "@lib/data/cart";
|
||||
import { createI18nServerInstance } from "~/lib/i18n/i18n.server";
|
||||
import { createOrder } from '~/lib/services/order.service';
|
||||
import { getOrderedAnalysisElementsIds, sendOrderToMedipost } from '~/lib/services/medipost.service';
|
||||
import { getOrderedAnalysisIds, sendOrderToMedipost } from '~/lib/services/medipost.service';
|
||||
import { createNotificationsApi } from '@kit/notifications/api';
|
||||
import { getSupabaseServerAdminClient } from '@kit/supabase/server-admin-client';
|
||||
import { AccountWithParams } from '@kit/accounts/api';
|
||||
@@ -20,12 +20,12 @@ const env = () => z
|
||||
.object({
|
||||
emailSender: z
|
||||
.string({
|
||||
required_error: 'EMAIL_SENDER is required',
|
||||
error: 'EMAIL_SENDER is required',
|
||||
})
|
||||
.min(1),
|
||||
siteUrl: z
|
||||
.string({
|
||||
required_error: 'NEXT_PUBLIC_SITE_URL is required',
|
||||
error: 'NEXT_PUBLIC_SITE_URL is required',
|
||||
})
|
||||
.min(1),
|
||||
})
|
||||
@@ -114,7 +114,7 @@ export async function processMontonioCallback(orderToken: string) {
|
||||
|
||||
|
||||
const medusaOrder = await placeOrder(cartId, { revalidateCacheTags: false });
|
||||
const orderedAnalysisElements = await getOrderedAnalysisElementsIds({ medusaOrder });
|
||||
const orderedAnalysisElements = await getOrderedAnalysisIds({ medusaOrder });
|
||||
const orderId = await createOrder({ medusaOrder, orderedAnalysisElements });
|
||||
|
||||
const { productTypes } = await listProductTypes();
|
||||
|
||||
@@ -1,17 +1,11 @@
|
||||
import { use } from 'react';
|
||||
|
||||
import { cookies } from 'next/headers';
|
||||
|
||||
import { retrieveCart } from '@lib/data/cart';
|
||||
import { StoreCart } from '@medusajs/types';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { UserWorkspaceContextProvider } from '@kit/accounts/components';
|
||||
import { AppLogo } from '@kit/shared/components/app-logo';
|
||||
import {
|
||||
pathsConfig,
|
||||
personalAccountNavigationConfig,
|
||||
} from '@kit/shared/config';
|
||||
import { pathsConfig } from '@kit/shared/config';
|
||||
import { Page, PageMobileNavigation, PageNavigation } from '@kit/ui/page';
|
||||
import { SidebarProvider } from '@kit/ui/shadcn-sidebar';
|
||||
|
||||
@@ -24,40 +18,11 @@ import { HomeSidebar } from '../_components/home-sidebar';
|
||||
import { loadUserWorkspace } from '../_lib/server/load-user-workspace';
|
||||
|
||||
function UserHomeLayout({ children }: React.PropsWithChildren) {
|
||||
const state = use(getLayoutState());
|
||||
|
||||
if (state.style === 'sidebar') {
|
||||
return <SidebarLayout>{children}</SidebarLayout>;
|
||||
}
|
||||
|
||||
return <HeaderLayout>{children}</HeaderLayout>;
|
||||
}
|
||||
|
||||
export default withI18n(UserHomeLayout);
|
||||
|
||||
function SidebarLayout({ children }: React.PropsWithChildren) {
|
||||
const workspace = use(loadUserWorkspace());
|
||||
const state = use(getLayoutState());
|
||||
|
||||
return (
|
||||
<UserWorkspaceContextProvider value={workspace}>
|
||||
<SidebarProvider defaultOpen={state.open}>
|
||||
<Page style={'sidebar'}>
|
||||
<PageNavigation>
|
||||
<HomeSidebar />
|
||||
</PageNavigation>
|
||||
|
||||
<PageMobileNavigation className={'flex items-center justify-between'}>
|
||||
<MobileNavigation workspace={workspace} cart={null} />
|
||||
</PageMobileNavigation>
|
||||
|
||||
{children}
|
||||
</Page>
|
||||
</SidebarProvider>
|
||||
</UserWorkspaceContextProvider>
|
||||
);
|
||||
}
|
||||
|
||||
function HeaderLayout({ children }: React.PropsWithChildren) {
|
||||
const workspace = use(loadUserWorkspace());
|
||||
const cart = use(retrieveCart());
|
||||
@@ -101,27 +66,3 @@ 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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client"
|
||||
|
||||
import { Badge, Heading, Text } from "@medusajs/ui"
|
||||
import { Badge, Text } from "@medusajs/ui"
|
||||
import { toast } from '@kit/ui/sonner';
|
||||
import React, { useActionState } from "react";
|
||||
|
||||
import { applyPromotions, submitPromotionForm } from "@lib/data/cart"
|
||||
@@ -31,11 +32,19 @@ export default function DiscountCode({ cart }: {
|
||||
|
||||
const removePromotionCode = async (code: string) => {
|
||||
const validPromotions = promotions.filter(
|
||||
(promotion) => promotion.code !== code
|
||||
(promotion) => promotion.code !== code,
|
||||
)
|
||||
|
||||
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'));
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -45,7 +54,14 @@ export default function DiscountCode({ cart }: {
|
||||
.map((p) => p.code!)
|
||||
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()
|
||||
}
|
||||
@@ -64,7 +80,7 @@ export default function DiscountCode({ cart }: {
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit((data) => addPromotionCode(data.code))}
|
||||
className="w-full mb-2 flex gap-x-2"
|
||||
className="w-full mb-2 flex gap-x-2 sm:flex-row flex-col gap-y-2"
|
||||
>
|
||||
<FormField
|
||||
name={'code'}
|
||||
@@ -87,16 +103,12 @@ export default function DiscountCode({ cart }: {
|
||||
</form>
|
||||
</Form>
|
||||
|
||||
<p className="text-sm text-muted-foreground">
|
||||
<Trans i18nKey={'cart:discountCode.subtitle'} />
|
||||
</p>
|
||||
|
||||
{promotions.length > 0 && (
|
||||
<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.length > 0 ? (
|
||||
<div className="w-full flex items-center mt-4">
|
||||
<div className="flex flex-col w-full gap-y-2">
|
||||
<p>
|
||||
<Trans i18nKey={'cart:discountCode.appliedCodes'} />
|
||||
</p>
|
||||
|
||||
{promotions.map((promotion) => {
|
||||
return (
|
||||
@@ -110,6 +122,7 @@ export default function DiscountCode({ cart }: {
|
||||
<Badge
|
||||
color={promotion.is_automatic ? "green" : "grey"}
|
||||
size="small"
|
||||
className="px-4"
|
||||
>
|
||||
{promotion.code}
|
||||
</Badge>{" "}
|
||||
@@ -151,7 +164,7 @@ export default function DiscountCode({ cart }: {
|
||||
>
|
||||
<Trash size={14} />
|
||||
<span className="sr-only">
|
||||
Remove discount code from order
|
||||
<Trans i18nKey={'cart:discountCode.remove'} />
|
||||
</span>
|
||||
</button>
|
||||
)}
|
||||
@@ -160,6 +173,10 @@ export default function DiscountCode({ cart }: {
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
<Trans i18nKey={'cart:discountCode.subtitle'} />
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -18,7 +18,7 @@ import { useTranslation } from "react-i18next";
|
||||
import { handleNavigateToPayment } from "@/lib/services/medusaCart.service";
|
||||
import AnalysisLocation from "./analysis-location";
|
||||
|
||||
const IS_DISCOUNT_SHOWN = false as boolean;
|
||||
const IS_DISCOUNT_SHOWN = true as boolean;
|
||||
|
||||
export default function Cart({
|
||||
cart,
|
||||
@@ -69,7 +69,7 @@ export default function Cart({
|
||||
|
||||
const hasCartItems = Array.isArray(cart.items) && cart.items.length > 0;
|
||||
const isLocationsShown = synlabAnalyses.length > 0;
|
||||
|
||||
|
||||
return (
|
||||
<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">
|
||||
@@ -77,28 +77,62 @@ export default function Cart({
|
||||
<CartItems cart={cart} items={ttoServiceItems} productColumnLabelKey="cart:items.ttoServices.productColumnLabel" />
|
||||
</div>
|
||||
{hasCartItems && (
|
||||
<div className="flex justify-end gap-x-4 px-6 py-4">
|
||||
<div className="mr-[36px]">
|
||||
<p className="ml-0 font-bold text-sm">
|
||||
<Trans i18nKey="cart:total" />
|
||||
</p>
|
||||
<>
|
||||
<div className="flex justify-end gap-x-4 px-6 pt-4">
|
||||
<div className="mr-[36px]">
|
||||
<p className="ml-0 font-bold text-sm text-muted-foreground">
|
||||
<Trans i18nKey="cart:subtotal" />
|
||||
</p>
|
||||
</div>
|
||||
<div className="mr-[116px]">
|
||||
<p className="text-sm">
|
||||
{formatCurrency({
|
||||
value: cart.subtotal,
|
||||
currencyCode: cart.currency_code,
|
||||
locale: language,
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mr-[116px]">
|
||||
<p className="text-sm">
|
||||
{formatCurrency({
|
||||
value: cart.total,
|
||||
currencyCode: cart.currency_code,
|
||||
locale: language,
|
||||
})}
|
||||
</p>
|
||||
<div className="flex justify-end gap-x-4 px-6 py-2">
|
||||
<div className="mr-[36px]">
|
||||
<p className="ml-0 font-bold text-sm text-muted-foreground">
|
||||
<Trans i18nKey="cart:promotionsTotal" />
|
||||
</p>
|
||||
</div>
|
||||
<div className="mr-[116px]">
|
||||
<p className="text-sm">
|
||||
{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 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 gap-y-6 py-8">
|
||||
<div className="flex sm:flex-row flex-col gap-y-6 py-8 gap-x-4">
|
||||
{IS_DISCOUNT_SHOWN && (
|
||||
<Card
|
||||
className="flex flex-col justify-between w-1/2"
|
||||
className="flex flex-col justify-between w-full sm:w-1/2"
|
||||
>
|
||||
<CardHeader className="pb-4">
|
||||
<h5>
|
||||
@@ -113,7 +147,7 @@ export default function Cart({
|
||||
|
||||
{isLocationsShown && (
|
||||
<Card
|
||||
className="flex flex-col justify-between w-1/2"
|
||||
className="flex flex-col justify-between w-full sm:w-1/2"
|
||||
>
|
||||
<CardHeader className="pb-4">
|
||||
<h5>
|
||||
|
||||
128
app/home/(user)/_components/dashboard-recommendations.tsx
Normal file
128
app/home/(user)/_components/dashboard-recommendations.tsx
Normal file
@@ -0,0 +1,128 @@
|
||||
'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>
|
||||
);
|
||||
}
|
||||
@@ -7,33 +7,41 @@ import { Database } from '@/packages/supabase/src/database.types';
|
||||
import { BlendingModeIcon, RulerHorizontalIcon } from '@radix-ui/react-icons';
|
||||
import {
|
||||
Activity,
|
||||
ChevronRight,
|
||||
Clock9,
|
||||
Droplets,
|
||||
Pill,
|
||||
Scale,
|
||||
TrendingUp,
|
||||
User,
|
||||
} from 'lucide-react';
|
||||
|
||||
import { InfoTooltip } from '@kit/shared/components/ui/info-tooltip';
|
||||
import { pathsConfig } from '@kit/shared/config';
|
||||
import { getPersonParameters } from '@kit/shared/utils';
|
||||
import { Button } from '@kit/ui/button';
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardProps,
|
||||
} from '@kit/ui/card';
|
||||
import { Trans } from '@kit/ui/trans';
|
||||
import { cn } from '@kit/ui/utils';
|
||||
|
||||
import { isNil } from 'lodash';
|
||||
import { BmiCategory } from '~/lib/types/bmi';
|
||||
import {
|
||||
bmiFromMetric,
|
||||
getBmiBackgroundColor,
|
||||
getBmiStatus,
|
||||
} from '~/lib/utils';
|
||||
import DashboardRecommendations from './dashboard-recommendations';
|
||||
|
||||
const getCardVariant = (isSuccess: boolean | null): CardProps['variant'] => {
|
||||
if (isSuccess === null) return 'default';
|
||||
if (isSuccess) return 'gradient-success';
|
||||
return 'gradient-destructive';
|
||||
};
|
||||
|
||||
const cards = ({
|
||||
gender,
|
||||
@@ -41,111 +49,91 @@ const cards = ({
|
||||
height,
|
||||
weight,
|
||||
bmiStatus,
|
||||
smoking,
|
||||
}: {
|
||||
gender?: string;
|
||||
age?: number;
|
||||
height?: number | null;
|
||||
weight?: number | null;
|
||||
bmiStatus: BmiCategory | null;
|
||||
smoking?: boolean | null;
|
||||
}) => [
|
||||
{
|
||||
title: 'dashboard:gender',
|
||||
description: gender ?? 'dashboard:male',
|
||||
icon: <User />,
|
||||
iconBg: 'bg-success',
|
||||
},
|
||||
{
|
||||
title: 'dashboard:age',
|
||||
description: age ? `${age}` : '-',
|
||||
icon: <Clock9 />,
|
||||
iconBg: 'bg-success',
|
||||
},
|
||||
{
|
||||
title: 'dashboard:height',
|
||||
description: height ? `${height}cm` : '-',
|
||||
icon: <RulerHorizontalIcon className="size-4" />,
|
||||
iconBg: 'bg-success',
|
||||
},
|
||||
{
|
||||
title: 'dashboard:weight',
|
||||
description: weight ? `${weight}kg` : '-',
|
||||
icon: <Scale />,
|
||||
iconBg: 'bg-success',
|
||||
},
|
||||
{
|
||||
title: 'dashboard:bmi',
|
||||
description: bmiFromMetric(weight || 0, height || 0).toString(),
|
||||
icon: <TrendingUp />,
|
||||
iconBg: getBmiBackgroundColor(bmiStatus),
|
||||
},
|
||||
{
|
||||
title: 'dashboard:bloodPressure',
|
||||
description: '-',
|
||||
icon: <Activity />,
|
||||
iconBg: 'bg-warning',
|
||||
},
|
||||
{
|
||||
title: 'dashboard:cholesterol',
|
||||
description: '-',
|
||||
icon: <BlendingModeIcon className="size-4" />,
|
||||
iconBg: 'bg-destructive',
|
||||
},
|
||||
{
|
||||
title: 'dashboard:ldlCholesterol',
|
||||
description: '-',
|
||||
icon: <Pill />,
|
||||
iconBg: 'bg-warning',
|
||||
},
|
||||
// {
|
||||
// title: 'Score 2',
|
||||
// description: 'Normis',
|
||||
// icon: <LineChart />,
|
||||
// iconBg: 'bg-success',
|
||||
// },
|
||||
// {
|
||||
// title: 'dashboard:smoking',
|
||||
// description: 'dashboard:respondToQuestion',
|
||||
// descriptionColor: 'text-primary',
|
||||
// icon: (
|
||||
// <Button size="icon" variant="outline" className="px-2 text-black">
|
||||
// <ChevronRight className="size-4 stroke-2" />
|
||||
// </Button>
|
||||
// ),
|
||||
// cardVariant: 'gradient-success' as CardProps['variant'],
|
||||
// },
|
||||
];
|
||||
{
|
||||
title: 'dashboard:gender',
|
||||
description: gender ?? 'dashboard:male',
|
||||
icon: <User />,
|
||||
iconBg: 'bg-success',
|
||||
},
|
||||
{
|
||||
title: 'dashboard:age',
|
||||
description: age ? `${age}` : '-',
|
||||
icon: <Clock9 />,
|
||||
iconBg: 'bg-success',
|
||||
},
|
||||
{
|
||||
title: 'dashboard:height',
|
||||
description: height ? `${height}cm` : '-',
|
||||
icon: <RulerHorizontalIcon className="size-4" />,
|
||||
iconBg: 'bg-success',
|
||||
},
|
||||
{
|
||||
title: 'dashboard:weight',
|
||||
description: weight ? `${weight}kg` : '-',
|
||||
icon: <Scale />,
|
||||
iconBg: 'bg-success',
|
||||
},
|
||||
{
|
||||
title: 'dashboard:bmi',
|
||||
description: bmiFromMetric(weight || 0, height || 0).toString(),
|
||||
icon: <TrendingUp />,
|
||||
iconBg: getBmiBackgroundColor(bmiStatus),
|
||||
},
|
||||
{
|
||||
title: 'dashboard:bloodPressure',
|
||||
description: '-',
|
||||
icon: <Activity />,
|
||||
iconBg: 'bg-warning',
|
||||
},
|
||||
{
|
||||
title: 'dashboard:cholesterol',
|
||||
description: '-',
|
||||
icon: <BlendingModeIcon className="size-4" />,
|
||||
iconBg: 'bg-destructive',
|
||||
},
|
||||
{
|
||||
title: 'dashboard:ldlCholesterol',
|
||||
description: '-',
|
||||
icon: <Pill />,
|
||||
iconBg: 'bg-warning',
|
||||
},
|
||||
// {
|
||||
// title: 'Score 2',
|
||||
// description: 'Normis',
|
||||
// icon: <LineChart />,
|
||||
// iconBg: 'bg-success',
|
||||
// },
|
||||
{
|
||||
title: 'dashboard:smoking',
|
||||
description:
|
||||
isNil(smoking)
|
||||
? 'dashboard:respondToQuestion'
|
||||
: !!smoking
|
||||
? 'common:yes'
|
||||
: 'common:no',
|
||||
descriptionColor: 'text-primary',
|
||||
icon:
|
||||
isNil(smoking) ? (
|
||||
<Link href={pathsConfig.app.personalAccountSettings}>
|
||||
<Button size="icon" variant="outline" className="px-2 text-black">
|
||||
<ChevronRight className="size-4 stroke-2" />
|
||||
</Button>
|
||||
</Link>
|
||||
) : null,
|
||||
cardVariant: getCardVariant(isNil(smoking) ? null : !smoking),
|
||||
},
|
||||
];
|
||||
|
||||
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',
|
||||
},
|
||||
];
|
||||
const IS_SHOWN_RECOMMENDATIONS = false as boolean;
|
||||
|
||||
export default function Dashboard({
|
||||
account,
|
||||
@@ -160,8 +148,8 @@ export default function Dashboard({
|
||||
const params = getPersonParameters(account.personal_code!);
|
||||
const bmiStatus = getBmiStatus(bmiThresholds, {
|
||||
age: params?.age || 0,
|
||||
height: account.account_params?.[0]?.height || 0,
|
||||
weight: account.account_params?.[0]?.weight || 0,
|
||||
height: account.accountParams?.height || 0,
|
||||
weight: account.accountParams?.weight || 0,
|
||||
});
|
||||
|
||||
return (
|
||||
@@ -170,21 +158,22 @@ export default function Dashboard({
|
||||
{cards({
|
||||
gender: params?.gender,
|
||||
age: params?.age,
|
||||
height: account.account_params?.[0]?.height,
|
||||
weight: account.account_params?.[0]?.weight,
|
||||
height: account.accountParams?.height,
|
||||
weight: account.accountParams?.weight,
|
||||
bmiStatus,
|
||||
smoking: account.accountParams?.isSmoker,
|
||||
}).map(
|
||||
({
|
||||
title,
|
||||
description,
|
||||
icon,
|
||||
iconBg,
|
||||
// cardVariant,
|
||||
cardVariant,
|
||||
// descriptionColor,
|
||||
}) => (
|
||||
<Card
|
||||
key={title}
|
||||
// variant={cardVariant}
|
||||
variant={cardVariant}
|
||||
className="flex flex-col justify-between"
|
||||
>
|
||||
<CardHeader className="items-end-safe">
|
||||
@@ -211,79 +200,7 @@ export default function Dashboard({
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
<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>
|
||||
{IS_SHOWN_RECOMMENDATIONS && <DashboardRecommendations />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -31,7 +31,8 @@ export async function HomeMenuNavigation(props: {
|
||||
})
|
||||
: 0;
|
||||
|
||||
const cartQuantityTotal = props.cart?.items?.reduce((acc, item) => acc + item.quantity, 0) ?? 0;
|
||||
const cartQuantityTotal =
|
||||
props.cart?.items?.reduce((acc, item) => acc + item.quantity, 0) ?? 0;
|
||||
const hasCartItems = cartQuantityTotal > 0;
|
||||
|
||||
return (
|
||||
@@ -39,15 +40,18 @@ export async function HomeMenuNavigation(props: {
|
||||
<div className={`flex items-center ${SIDEBAR_WIDTH_PROPERTY}`}>
|
||||
<AppLogo href={pathsConfig.app.home} />
|
||||
</div>
|
||||
<Search
|
||||
{/* TODO: add search functionality */}
|
||||
{/* <Search
|
||||
className="flex grow"
|
||||
startElement={<Trans i18nKey="common:search" values={{ end: '...' }} />}
|
||||
/>
|
||||
/> */}
|
||||
|
||||
<div className="flex items-center justify-end gap-3">
|
||||
{/* TODO: add wallet functionality
|
||||
<Card className="px-6 py-2">
|
||||
<span>€ {Number(0).toFixed(2).replace('.', ',')}</span>
|
||||
</Card>
|
||||
*/}
|
||||
{hasCartItems && (
|
||||
<Button
|
||||
className="relative mr-0 h-10 cursor-pointer border-1 px-4 py-2"
|
||||
@@ -56,7 +60,7 @@ export async function HomeMenuNavigation(props: {
|
||||
<span className="flex items-center text-nowrap">{totalValue}</span>
|
||||
</Button>
|
||||
)}
|
||||
<Link href="/home/cart">
|
||||
<Link href={pathsConfig.app.cart}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="relative mr-0 h-10 cursor-pointer border-1 px-4 py-2"
|
||||
|
||||
@@ -4,11 +4,13 @@ import { useMemo } from 'react';
|
||||
|
||||
import Link from 'next/link';
|
||||
|
||||
import SignOutDropdownItem from '@kit/shared/components/sign-out-dropdown-item';
|
||||
import { StoreCart } from '@medusajs/types';
|
||||
import { Cross, LogOut, Menu, Shield, ShoppingCart } from 'lucide-react';
|
||||
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 DropdownLink from '@kit/shared/components/ui/dropdown-link';
|
||||
import {
|
||||
pathsConfig,
|
||||
personalAccountNavigationConfig,
|
||||
@@ -91,7 +93,7 @@ export function HomeMobileNavigation(props: {
|
||||
<If condition={props.cart && hasCartItems}>
|
||||
<DropdownMenuGroup>
|
||||
<DropdownLink
|
||||
path="/home/cart"
|
||||
path={pathsConfig.app.cart}
|
||||
label="common:shoppingCartCount"
|
||||
Icon={<ShoppingCart className="stroke-[1.5px]" />}
|
||||
labelOptions={{ count: cartQuantityTotal }}
|
||||
@@ -145,49 +147,4 @@ 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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { HeartPulse, Loader2, ShoppingCart } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { Button } from '@kit/ui/button';
|
||||
import {
|
||||
@@ -15,12 +16,14 @@ import { handleAddToCart } from '~/lib/services/medusaCart.service';
|
||||
import { InfoTooltip } from '@kit/shared/components/ui/info-tooltip';
|
||||
import { Trans } from '@kit/ui/trans';
|
||||
import { toast } from '@kit/ui/sonner';
|
||||
import { formatCurrency } from '@/packages/shared/src/utils';
|
||||
|
||||
export type OrderAnalysisCard = Pick<
|
||||
StoreProduct, 'title' | 'description' | 'subtitle'
|
||||
> & {
|
||||
isAvailable: boolean;
|
||||
variant: { id: string };
|
||||
price: number | null;
|
||||
};
|
||||
|
||||
export default function OrderAnalysesCards({
|
||||
@@ -30,23 +33,26 @@ export default function OrderAnalysesCards({
|
||||
analyses: OrderAnalysisCard[];
|
||||
countryCode: string;
|
||||
}) {
|
||||
const [isAddingToCart, setIsAddingToCart] = useState(false);
|
||||
|
||||
const { i18n: { language } } = useTranslation()
|
||||
|
||||
const [variantAddingToCart, setVariantAddingToCart] = useState<string | null>(null);
|
||||
const handleSelect = async (variantId: string) => {
|
||||
if (isAddingToCart) {
|
||||
if (variantAddingToCart) {
|
||||
return null;
|
||||
}
|
||||
|
||||
setIsAddingToCart(true);
|
||||
setVariantAddingToCart(variantId);
|
||||
try {
|
||||
await handleAddToCart({
|
||||
selectedVariant: { id: variantId },
|
||||
countryCode,
|
||||
});
|
||||
toast.success(<Trans i18nKey={'order-analysis:analysisAddedToCart'} />);
|
||||
setIsAddingToCart(false);
|
||||
setVariantAddingToCart(null);
|
||||
} catch (e) {
|
||||
toast.error(<Trans i18nKey={'order-analysis:analysisAddToCartError'} />);
|
||||
setIsAddingToCart(false);
|
||||
setVariantAddingToCart(null);
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
@@ -59,7 +65,15 @@ export default function OrderAnalysesCards({
|
||||
description,
|
||||
subtitle,
|
||||
isAvailable,
|
||||
price,
|
||||
}) => {
|
||||
const formattedPrice = typeof price === 'number'
|
||||
? formatCurrency({
|
||||
currencyCode: 'eur',
|
||||
locale: language,
|
||||
value: price,
|
||||
})
|
||||
: null;
|
||||
return (
|
||||
<Card
|
||||
key={title}
|
||||
@@ -80,7 +94,7 @@ export default function OrderAnalysesCards({
|
||||
className="px-2 text-black"
|
||||
onClick={() => handleSelect(variant.id)}
|
||||
>
|
||||
{isAddingToCart ? <Loader2 className="size-4 stroke-2 animate-spin" /> : <ShoppingCart className="size-4 stroke-2" />}
|
||||
{variantAddingToCart ? <Loader2 className="size-4 stroke-2 animate-spin" /> : <ShoppingCart className="size-4 stroke-2" />}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
@@ -91,7 +105,14 @@ export default function OrderAnalysesCards({
|
||||
{description && (
|
||||
<>
|
||||
{' '}
|
||||
<InfoTooltip content={`${description}`} />
|
||||
<InfoTooltip
|
||||
content={
|
||||
<div className='flex flex-col gap-2'>
|
||||
<span>{formattedPrice}</span>
|
||||
<span>{description}</span>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</h5>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { cache } from 'react';
|
||||
|
||||
import { requireUserInServerComponent } from '@/lib/server/require-user-in-server-component';
|
||||
|
||||
import { createAccountsApi } from '@kit/accounts/api';
|
||||
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { cache } from 'react';
|
||||
|
||||
import { getProductCategories } from '@lib/data/categories';
|
||||
import { listProductTypes } from '@lib/data/products';
|
||||
import { listProducts, listProductTypes } from '@lib/data/products';
|
||||
import { listRegions } from '@lib/data/regions';
|
||||
|
||||
import { OrderAnalysisCard } from '../../_components/order-analyses-cards';
|
||||
import { ServiceCategory } from '../../_components/service-categories';
|
||||
|
||||
async function countryCodesLoader() {
|
||||
const countryCodes = await listRegions().then((regions) =>
|
||||
@@ -39,13 +38,20 @@ async function analysesLoader() {
|
||||
const category = productCategories.find(
|
||||
({ metadata }) => metadata?.page === 'order-analysis',
|
||||
);
|
||||
const categoryProducts = category
|
||||
? await listProducts({
|
||||
countryCode,
|
||||
queryParams: { limit: 100, category_id: category.id },
|
||||
})
|
||||
: null;
|
||||
|
||||
const serviceCategories = productCategories.filter(
|
||||
({ parent_category }) => parent_category?.handle === 'tto-categories',
|
||||
);
|
||||
|
||||
return {
|
||||
analyses:
|
||||
category?.products?.map<OrderAnalysisCard>(
|
||||
categoryProducts?.response.products.map<OrderAnalysisCard>(
|
||||
({ title, description, subtitle, variants, status, metadata }) => {
|
||||
const variant = variants![0]!;
|
||||
return {
|
||||
@@ -57,6 +63,7 @@ async function analysesLoader() {
|
||||
},
|
||||
isAvailable:
|
||||
status === 'published' && !!metadata?.analysisIdOriginal,
|
||||
price: variant.calculated_price?.calculated_amount ?? null,
|
||||
};
|
||||
},
|
||||
) ?? [],
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
'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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
259
app/home/(user)/settings/_components/account-settings-form.tsx
Normal file
259
app/home/(user)/settings/_components/account-settings-form.tsx
Normal file
@@ -0,0 +1,259 @@
|
||||
'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>
|
||||
);
|
||||
}
|
||||
152
app/home/(user)/settings/_components/settings-navigation.tsx
Normal file
152
app/home/(user)/settings/_components/settings-navigation.tsx
Normal file
@@ -0,0 +1,152 @@
|
||||
'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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
55
app/home/(user)/settings/_components/settings-sidebar.tsx
Normal file
55
app/home/(user)/settings/_components/settings-sidebar.tsx
Normal file
@@ -0,0 +1,55 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
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>;
|
||||
21
app/home/(user)/settings/_lib/account-settings.schema.ts
Normal file
21
app/home/(user)/settings/_lib/account-settings.schema.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
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>;
|
||||
66
app/home/(user)/settings/_lib/server/actions.ts
Normal file
66
app/home/(user)/settings/_lib/server/actions.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
'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 }),
|
||||
},
|
||||
);
|
||||
@@ -1,23 +1,69 @@
|
||||
import { AppBreadcrumbs } from '@kit/ui/app-breadcrumbs';
|
||||
import { Trans } from '@kit/ui/trans';
|
||||
import { use } from 'react';
|
||||
|
||||
import { retrieveCart } from '@lib/data/cart';
|
||||
import { StoreCart } from '@medusajs/types';
|
||||
|
||||
import { UserWorkspaceContextProvider } from '@kit/accounts/components';
|
||||
import { AppLogo } from '@kit/shared/components/app-logo';
|
||||
import { pathsConfig } from '@kit/shared/config';
|
||||
import { Page, PageMobileNavigation, PageNavigation } from '@kit/ui/page';
|
||||
import { SidebarProvider } from '@kit/ui/shadcn-sidebar';
|
||||
|
||||
import { 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';
|
||||
|
||||
// local imports
|
||||
import { HomeLayoutPageHeader } from '../_components/home-page-header';
|
||||
|
||||
function UserSettingsLayout(props: React.PropsWithChildren) {
|
||||
return (
|
||||
<>
|
||||
<HomeLayoutPageHeader
|
||||
title={<Trans i18nKey={'account:routes.settings'} />}
|
||||
description={<AppBreadcrumbs />}
|
||||
/>
|
||||
|
||||
{props.children}
|
||||
</>
|
||||
);
|
||||
function UserSettingsLayout({ children }: React.PropsWithChildren) {
|
||||
return <HeaderLayout>{children}</HeaderLayout>;
|
||||
}
|
||||
|
||||
export default withI18n(UserSettingsLayout);
|
||||
|
||||
function HeaderLayout({ children }: React.PropsWithChildren) {
|
||||
const workspace = use(loadUserWorkspace());
|
||||
const cart = use(retrieveCart());
|
||||
|
||||
|
||||
return (
|
||||
<UserWorkspaceContextProvider value={workspace}>
|
||||
<Page style={'header'}>
|
||||
<PageNavigation>
|
||||
<HomeMenuNavigation workspace={workspace} cart={cart} />
|
||||
</PageNavigation>
|
||||
|
||||
<PageMobileNavigation className={'flex items-center justify-between'}>
|
||||
<MobileNavigation workspace={workspace} cart={cart} />
|
||||
</PageMobileNavigation>
|
||||
|
||||
<SidebarProvider defaultOpen>
|
||||
<Page style={'sidebar'}>
|
||||
<PageNavigation>
|
||||
<SettingsSidebar />
|
||||
</PageNavigation>
|
||||
<div className="md:mt-28 min-w-full min-h-full">{children}</div>
|
||||
</Page>
|
||||
</SidebarProvider>
|
||||
</Page>
|
||||
</UserWorkspaceContextProvider>
|
||||
);
|
||||
}
|
||||
|
||||
function MobileNavigation({
|
||||
workspace,
|
||||
cart,
|
||||
}: {
|
||||
workspace: Awaited<ReturnType<typeof loadUserWorkspace>>;
|
||||
cart: StoreCart | null;
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
<AppLogo href={pathsConfig.app.home} />
|
||||
|
||||
<SettingsMobileNavigation workspace={workspace} cart={cart} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,28 +1,11 @@
|
||||
import { use } from 'react';
|
||||
|
||||
import { PersonalAccountSettingsContainer } from '@kit/accounts/personal-account-settings';
|
||||
import {
|
||||
authConfig,
|
||||
featureFlagsConfig,
|
||||
pathsConfig,
|
||||
} from '@kit/shared/config';
|
||||
import { PageBody } from '@kit/ui/page';
|
||||
|
||||
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
|
||||
import { requireUserInServerComponent } from '~/lib/server/require-user-in-server-component';
|
||||
import { withI18n } from '~/lib/i18n/with-i18n';
|
||||
|
||||
const features = {
|
||||
enableAccountDeletion: featureFlagsConfig.enableAccountDeletion,
|
||||
enablePasswordUpdate: authConfig.providers.password,
|
||||
};
|
||||
|
||||
const callbackPath = pathsConfig.auth.callback;
|
||||
const accountHomePath = pathsConfig.app.accountHome;
|
||||
|
||||
const paths = {
|
||||
callback: callbackPath + `?next=${accountHomePath}`,
|
||||
};
|
||||
import { loadCurrentUserAccount } from '../_lib/server/load-user-account';
|
||||
import AccountSettingsForm from './_components/account-settings-form';
|
||||
import SettingsSectionHeader from './_components/settings-section-header';
|
||||
|
||||
export const generateMetadata = async () => {
|
||||
const i18n = await createI18nServerInstance();
|
||||
@@ -33,17 +16,18 @@ export const generateMetadata = async () => {
|
||||
};
|
||||
};
|
||||
|
||||
function PersonalAccountSettingsPage() {
|
||||
const user = use(requireUserInServerComponent());
|
||||
|
||||
async function PersonalAccountSettingsPage() {
|
||||
const account = await loadCurrentUserAccount();
|
||||
return (
|
||||
<PageBody>
|
||||
<div className={'flex w-full flex-1 flex-col lg:max-w-2xl'}>
|
||||
<PersonalAccountSettingsContainer
|
||||
userId={user.id}
|
||||
features={features}
|
||||
paths={paths}
|
||||
/>
|
||||
<div className="mx-auto w-full bg-white p-6">
|
||||
<div className="space-y-6">
|
||||
<SettingsSectionHeader
|
||||
titleKey="account:accountTabLabel"
|
||||
descriptionKey="account:accountTabDescription"
|
||||
/>
|
||||
<AccountSettingsForm account={account} />
|
||||
</div>
|
||||
</div>
|
||||
</PageBody>
|
||||
);
|
||||
|
||||
24
app/home/(user)/settings/preferences/page.tsx
Normal file
24
app/home/(user)/settings/preferences/page.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
17
app/home/(user)/settings/security/page.tsx
Normal file
17
app/home/(user)/settings/security/page.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -1,9 +1,10 @@
|
||||
'use client';
|
||||
|
||||
import Link from 'next/link';
|
||||
import DropdownLink from '@kit/shared/components/ui/dropdown-link';
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
import { Home, LogOut, Menu } from 'lucide-react';
|
||||
import SignOutDropdownItem from '@kit/shared/components/sign-out-dropdown-item';
|
||||
import { Home, Menu } from 'lucide-react';
|
||||
|
||||
import { AccountSelector } from '@kit/accounts/account-selector';
|
||||
import {
|
||||
@@ -92,47 +93,7 @@ export const TeamAccountLayoutMobileNavigation = (
|
||||
);
|
||||
};
|
||||
|
||||
function DropdownLink(
|
||||
props: React.PropsWithChildren<{
|
||||
path: string;
|
||||
label: string;
|
||||
Icon: React.ReactNode;
|
||||
}>,
|
||||
) {
|
||||
return (
|
||||
<DropdownMenuItem asChild>
|
||||
<Link
|
||||
href={props.path}
|
||||
className={'flex h-12 w-full items-center gap-x-3 px-3'}
|
||||
>
|
||||
{props.Icon}
|
||||
|
||||
<span>
|
||||
<Trans i18nKey={props.label} defaults={props.label} />
|
||||
</span>
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
}
|
||||
|
||||
function SignOutDropdownItem(
|
||||
props: React.PropsWithChildren<{
|
||||
onSignOut: () => unknown;
|
||||
}>,
|
||||
) {
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
className={'flex h-12 w-full items-center space-x-2'}
|
||||
onClick={props.onSignOut}
|
||||
>
|
||||
<LogOut className={'h-4'} />
|
||||
|
||||
<span>
|
||||
<Trans i18nKey={'common:signOut'} />
|
||||
</span>
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
}
|
||||
|
||||
function TeamAccountsModal(props: {
|
||||
accounts: Accounts;
|
||||
|
||||
@@ -41,6 +41,7 @@ export const defaultI18nNamespaces = [
|
||||
'orders',
|
||||
'analysis-results',
|
||||
'doctor',
|
||||
'error',
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
import type { Tables } from '@/packages/supabase/src/database.types';
|
||||
|
||||
import { AccountWithParams } from '@kit/accounts/api';
|
||||
import { getSupabaseServerAdminClient } from '@kit/supabase/server-admin-client';
|
||||
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
||||
|
||||
import { AccountSettings } from '~/home/(user)/settings/_lib/account-settings.schema';
|
||||
|
||||
import { AccountPreferences } from '../../app/home/(user)/settings/_lib/account-preferences.schema';
|
||||
import { updateCustomer } from '../../packages/features/medusa-storefront/src/lib/data';
|
||||
|
||||
type Account = Tables<{ schema: 'medreport' }, 'accounts'>;
|
||||
type Membership = Tables<{ schema: 'medreport' }, 'accounts_memberships'>;
|
||||
|
||||
@@ -61,7 +67,9 @@ export async function getDoctorAccounts() {
|
||||
}
|
||||
|
||||
export async function getAssignedDoctorAccount(analysisOrderId: number) {
|
||||
const { data: doctorUser } = await getSupabaseServerAdminClient()
|
||||
const supabase = getSupabaseServerAdminClient();
|
||||
|
||||
const { data: doctorUser } = await supabase
|
||||
.schema('medreport')
|
||||
.from('doctor_analysis_feedback')
|
||||
.select('doctor_user_id')
|
||||
@@ -73,7 +81,7 @@ export async function getAssignedDoctorAccount(analysisOrderId: number) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { data } = await getSupabaseServerAdminClient()
|
||||
const { data } = await supabase
|
||||
.schema('medreport')
|
||||
.from('accounts')
|
||||
.select('email')
|
||||
@@ -81,3 +89,58 @@ export async function getAssignedDoctorAccount(analysisOrderId: number) {
|
||||
|
||||
return { email: data?.[0]?.email };
|
||||
}
|
||||
|
||||
export async function updatePersonalAccount(
|
||||
accountId: string,
|
||||
account: AccountSettings,
|
||||
) {
|
||||
const supabase = getSupabaseServerClient();
|
||||
|
||||
return Promise.all([
|
||||
supabase
|
||||
.schema('medreport')
|
||||
.from('accounts')
|
||||
.update({
|
||||
name: account.firstName,
|
||||
last_name: account.lastName,
|
||||
email: account.email,
|
||||
phone: account.phone,
|
||||
})
|
||||
.eq('id', accountId)
|
||||
.throwOnError(),
|
||||
supabase
|
||||
.schema('medreport')
|
||||
.from('account_params')
|
||||
.upsert(
|
||||
{
|
||||
height: account.accountParams.height,
|
||||
weight: account.accountParams.weight,
|
||||
is_smoker: account.accountParams.isSmoker,
|
||||
},
|
||||
{ onConflict: 'account_id' },
|
||||
)
|
||||
.throwOnError(),
|
||||
updateCustomer({
|
||||
first_name: account.firstName,
|
||||
last_name: account.lastName,
|
||||
phone: account.phone,
|
||||
}),
|
||||
]);
|
||||
}
|
||||
export async function updatePersonalAccountPreferences(
|
||||
accountId: string,
|
||||
preferences: AccountPreferences,
|
||||
) {
|
||||
const supabase = getSupabaseServerClient();
|
||||
|
||||
return supabase
|
||||
.schema('medreport')
|
||||
.from('accounts')
|
||||
.update({
|
||||
preferred_locale: preferences.preferredLanguage,
|
||||
has_consent_anonymized_company_statistics:
|
||||
preferences.isConsentToAnonymizedCompanyStatistics,
|
||||
})
|
||||
.eq('id', accountId)
|
||||
.throwOnError();
|
||||
}
|
||||
|
||||
@@ -105,12 +105,18 @@ export const createMedusaSyncSuccessEntry = async () => {
|
||||
});
|
||||
}
|
||||
|
||||
export async function getAnalyses({ ids }: { ids: number[] }): Promise<AnalysesWithGroupsAndElements> {
|
||||
const { data } = await getSupabaseServerAdminClient()
|
||||
export async function getAnalyses({ ids, originalIds }: { ids?: number[], originalIds?: string[] }): Promise<AnalysesWithGroupsAndElements> {
|
||||
const query = getSupabaseServerAdminClient()
|
||||
.schema('medreport')
|
||||
.from('analyses')
|
||||
.select(`*, analysis_elements(*, analysis_groups(*))`)
|
||||
.in('id', ids);
|
||||
.select(`*, analysis_elements(*, analysis_groups(*))`);
|
||||
if (Array.isArray(ids)) {
|
||||
query.in('id', ids);
|
||||
}
|
||||
if (Array.isArray(originalIds)) {
|
||||
query.in('analysis_id_original', originalIds);
|
||||
}
|
||||
const { data } = await query.throwOnError();
|
||||
|
||||
return data as unknown as AnalysesWithGroupsAndElements;
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
getClientInstitution,
|
||||
getClientPerson,
|
||||
getConfidentiality,
|
||||
getOrderEnteredPerson,
|
||||
getPais,
|
||||
getPatient,
|
||||
getProviderInstitution,
|
||||
@@ -97,7 +98,7 @@ export async function getLatestPublicMessageListItem() {
|
||||
Action: MedipostAction.GetPublicMessageList,
|
||||
User: USER,
|
||||
Password: PASSWORD,
|
||||
Sender: 'syndev',
|
||||
Sender: RECIPIENT,
|
||||
// LastChecked (date+time) can be used here to get only messages since the last check - add when cron is created
|
||||
// MessageType check only for messages of certain type
|
||||
},
|
||||
@@ -553,14 +554,12 @@ export async function composeOrderXML({
|
||||
${getPais(USER, RECIPIENT, orderCreatedAt, orderId)}
|
||||
<Tellimus cito="EI">
|
||||
<ValisTellimuseId>${orderId}</ValisTellimuseId>
|
||||
<!--<TellijaAsutus>-->
|
||||
${getClientInstitution()}
|
||||
<!--<TeostajaAsutus>-->
|
||||
${getProviderInstitution()}
|
||||
<!--<TellijaIsik>-->
|
||||
${getClientPerson(person)}
|
||||
${getClientPerson()}
|
||||
${getOrderEnteredPerson()}
|
||||
<TellijaMarkused>${comment ?? ''}</TellijaMarkused>
|
||||
${getPatient(person)}
|
||||
${getPatient(person)}
|
||||
${getConfidentiality()}
|
||||
${specimenSection.join('')}
|
||||
${analysisSection?.join('')}
|
||||
@@ -666,7 +665,7 @@ async function syncPrivateMessage({
|
||||
unit: element.Mootyhik ?? null,
|
||||
original_response_element: element,
|
||||
analysis_name: element.UuringNimi || element.KNimetus,
|
||||
comment: element.UuringuKommentaar
|
||||
comment: element.UuringuKommentaar ?? '',
|
||||
})),
|
||||
);
|
||||
}
|
||||
@@ -715,7 +714,7 @@ export async function sendOrderToMedipost({
|
||||
orderedAnalysisElements,
|
||||
}: {
|
||||
medusaOrderId: string;
|
||||
orderedAnalysisElements: { analysisElementId: number }[];
|
||||
orderedAnalysisElements: { analysisElementId?: number; analysisId?: number }[];
|
||||
}) {
|
||||
const medreportOrder = await getOrder({ medusaOrderId });
|
||||
const account = await getAccountAdmin({ primaryOwnerUserId: medreportOrder.user_id });
|
||||
@@ -727,8 +726,8 @@ export async function sendOrderToMedipost({
|
||||
lastName: account.last_name ?? '',
|
||||
phone: account.phone ?? '',
|
||||
},
|
||||
orderedAnalysisElementsIds: orderedAnalysisElements.map(({ analysisElementId }) => analysisElementId),
|
||||
orderedAnalysesIds: [],
|
||||
orderedAnalysisElementsIds: orderedAnalysisElements.map(({ analysisElementId }) => analysisElementId).filter(Boolean) as number[],
|
||||
orderedAnalysesIds: orderedAnalysisElements.map(({ analysisId }) => analysisId).filter(Boolean) as number[],
|
||||
orderId: medusaOrderId,
|
||||
orderCreatedAt: new Date(medreportOrder.created_at),
|
||||
comment: '',
|
||||
@@ -784,12 +783,13 @@ export async function sendOrderToMedipost({
|
||||
await updateOrderStatus({ medusaOrderId, orderStatus: 'PROCESSING' });
|
||||
}
|
||||
|
||||
export async function getOrderedAnalysisElementsIds({
|
||||
export async function getOrderedAnalysisIds({
|
||||
medusaOrder,
|
||||
}: {
|
||||
medusaOrder: StoreOrder;
|
||||
}): Promise<{
|
||||
analysisElementId: number;
|
||||
analysisElementId?: number;
|
||||
analysisId?: number;
|
||||
}[]> {
|
||||
const countryCodes = await listRegions();
|
||||
const countryCode = countryCodes[0]!.countries![0]!.iso_2!;
|
||||
@@ -802,6 +802,14 @@ export async function getOrderedAnalysisElementsIds({
|
||||
return analysisElements.map(({ id }) => ({ analysisElementId: id }));
|
||||
}
|
||||
|
||||
async function getOrderedAnalyses(medusaOrder: StoreOrder) {
|
||||
const originalIds = (medusaOrder?.items ?? [])
|
||||
.map((a) => a.product?.metadata?.analysisIdOriginal)
|
||||
.filter((a) => typeof a === 'string') as string[];
|
||||
const analyses = await getAnalyses({ originalIds });
|
||||
return analyses.map(({ id }) => ({ analysisId: id }));
|
||||
}
|
||||
|
||||
async function getOrderedAnalysisPackages(medusaOrder: StoreOrder) {
|
||||
const orderedPackages = (medusaOrder?.items ?? []).filter(({ product }) => product?.handle.startsWith(ANALYSIS_PACKAGE_HANDLE_PREFIX));
|
||||
const orderedPackageIds = orderedPackages.map(({ product }) => product?.id).filter(Boolean) as string[];
|
||||
@@ -841,12 +849,13 @@ export async function getOrderedAnalysisElementsIds({
|
||||
return analysisElements.map(({ id }) => ({ analysisElementId: id }));
|
||||
}
|
||||
|
||||
const [analysisPackageElements, orderedAnalysisElements] = await Promise.all([
|
||||
const [analysisPackageElements, orderedAnalysisElements, orderedAnalyses] = await Promise.all([
|
||||
getOrderedAnalysisPackages(medusaOrder),
|
||||
getOrderedAnalysisElements(medusaOrder),
|
||||
getOrderedAnalyses(medusaOrder),
|
||||
]);
|
||||
|
||||
return [...analysisPackageElements, ...orderedAnalysisElements];
|
||||
return [...analysisPackageElements, ...orderedAnalysisElements, ...orderedAnalyses];
|
||||
}
|
||||
|
||||
export async function createMedipostActionLog({
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import {
|
||||
getClientInstitution,
|
||||
getClientPerson,
|
||||
getOrderEnteredPerson,
|
||||
getPais,
|
||||
getPatient,
|
||||
getProviderInstitution,
|
||||
@@ -104,7 +105,8 @@ export async function composeOrderTestResponseXML({
|
||||
<ValisTellimuseId>${orderId}</ValisTellimuseId>
|
||||
${getClientInstitution({ index: 1 })}
|
||||
${getProviderInstitution({ index: 1 })}
|
||||
${getClientPerson(person)}
|
||||
${getClientPerson()}
|
||||
${getOrderEnteredPerson()}
|
||||
<TellijaMarkused>Siia tuleb tellija poolne märkus</TellijaMarkused>
|
||||
|
||||
${getPatient(person)}
|
||||
|
||||
@@ -16,12 +16,12 @@ const env = () =>
|
||||
.object({
|
||||
medusaBackendPublicUrl: z
|
||||
.string({
|
||||
required_error: 'MEDUSA_BACKEND_PUBLIC_URL is required',
|
||||
error: 'MEDUSA_BACKEND_PUBLIC_URL is required',
|
||||
})
|
||||
.min(1),
|
||||
siteUrl: z
|
||||
.string({
|
||||
required_error: 'NEXT_PUBLIC_SITE_URL is required',
|
||||
error: 'NEXT_PUBLIC_SITE_URL is required',
|
||||
})
|
||||
.min(1),
|
||||
})
|
||||
|
||||
@@ -10,7 +10,7 @@ export async function createOrder({
|
||||
orderedAnalysisElements,
|
||||
}: {
|
||||
medusaOrder: StoreOrder;
|
||||
orderedAnalysisElements: { analysisElementId: number }[];
|
||||
orderedAnalysisElements: { analysisElementId?: number; analysisId?: number }[];
|
||||
}) {
|
||||
const supabase = getSupabaseServerClient();
|
||||
|
||||
@@ -21,8 +21,8 @@ export async function createOrder({
|
||||
const orderResult = await supabase.schema('medreport')
|
||||
.from('analysis_orders')
|
||||
.insert({
|
||||
analysis_element_ids: orderedAnalysisElements.map(({ analysisElementId }) => analysisElementId),
|
||||
analysis_ids: [],
|
||||
analysis_element_ids: orderedAnalysisElements.map(({ analysisElementId }) => analysisElementId).filter(Boolean) as number[],
|
||||
analysis_ids: orderedAnalysisElements.map(({ analysisId }) => analysisId).filter(Boolean) as number[],
|
||||
status: 'QUEUED',
|
||||
user_id: user.id,
|
||||
medusa_order_id: medusaOrder.id,
|
||||
|
||||
@@ -21,70 +21,48 @@ export const getPais = (
|
||||
<Saaja>${recipient}</Saaja>
|
||||
<Aeg>${format(createdAt, DATE_TIME_FORMAT)}</Aeg>
|
||||
<SaadetisId>${orderId}</SaadetisId>
|
||||
<Email>argo@medreport.ee</Email>
|
||||
<Email>info@medreport.ee</Email>
|
||||
</Pais>`;
|
||||
};
|
||||
|
||||
export const getClientInstitution = ({ index }: { index?: number } = {}) => {
|
||||
if (isProd) {
|
||||
// return correct data
|
||||
}
|
||||
return `<Asutus tyyp="TELLIJA" ${index ? ` jarjenumber="${index}"` : ''}>
|
||||
<AsutuseId>16381793</AsutuseId>
|
||||
<AsutuseNimi>MedReport OÜ</AsutuseNimi>
|
||||
<AsutuseKood>TSU</AsutuseKood>
|
||||
<AsutuseKood>MRP</AsutuseKood>
|
||||
<Telefon>+37258871517</Telefon>
|
||||
</Asutus>`;
|
||||
};
|
||||
|
||||
export const getProviderInstitution = ({ index }: { index?: number } = {}) => {
|
||||
if (isProd) {
|
||||
// return correct data
|
||||
}
|
||||
return `<Asutus tyyp="TEOSTAJA" ${index ? ` jarjenumber="${index}"` : ''}>
|
||||
<AsutuseId>11107913</AsutuseId>
|
||||
<AsutuseNimi>Synlab HTI Tallinn</AsutuseNimi>
|
||||
<AsutuseKood>SLA</AsutuseKood>
|
||||
<AsutuseNimi>Synlab Eesti OÜ</AsutuseNimi>
|
||||
<AsutuseKood>HTI</AsutuseKood>
|
||||
<AllyksuseNimi>Synlab HTI Tallinn</AllyksuseNimi>
|
||||
<Telefon>+3723417123</Telefon>
|
||||
<Telefon>+37217123</Telefon>
|
||||
</Asutus>`;
|
||||
};
|
||||
|
||||
export const getClientPerson = ({
|
||||
idCode,
|
||||
firstName,
|
||||
lastName,
|
||||
phone,
|
||||
}: {
|
||||
idCode: string,
|
||||
firstName: string,
|
||||
lastName: string,
|
||||
phone: string,
|
||||
}) => {
|
||||
if (isProd) {
|
||||
// return correct data
|
||||
}
|
||||
export const getClientPerson = () => {
|
||||
return `<Personal tyyp="TELLIJA" jarjenumber="1">
|
||||
<PersonalOID>1.3.6.1.4.1.28284.6.2.4.9</PersonalOID>
|
||||
<PersonalKood>${idCode}</PersonalKood>
|
||||
<PersonalPerekonnaNimi>${lastName}</PersonalPerekonnaNimi>
|
||||
<PersonalEesNimi>${firstName}</PersonalEesNimi>
|
||||
${phone ? `<Telefon>${phone.startsWith('+372') ? phone : `+372${phone}`}</Telefon>` : ''}
|
||||
<PersonalKood>D07907</PersonalKood>
|
||||
<PersonalPerekonnaNimi>Eduard</PersonalPerekonnaNimi>
|
||||
<PersonalEesNimi>Tsvetkov</PersonalEesNimi>
|
||||
<Telefon>+37258131202</Telefon>
|
||||
</Personal>`;
|
||||
};
|
||||
|
||||
// export const getOrderEnteredPerson = () => {
|
||||
// if (isProd) {
|
||||
// // return correct data
|
||||
// }
|
||||
// return `<Personal tyyp="SISESTAJA" jarjenumber="1">
|
||||
// <PersonalOID>1.3.6.1.4.1.28284.6.2.4.9</PersonalOID>
|
||||
// <PersonalKood>D07907</PersonalKood>
|
||||
// <PersonalPerekonnaNimi>Eduard</PersonalPerekonnaNimi>
|
||||
// <PersonalEesNimi>Tsvetkov</PersonalEesNimi>
|
||||
// <Telefon>+37258131202</Telefon>
|
||||
// </Personal>`;
|
||||
// };
|
||||
export const getOrderEnteredPerson = () => {
|
||||
return `<Personal tyyp="SISESTAJA" jarjenumber="2">
|
||||
<PersonalOID>1.3.6.1.4.1.28284.6.2.4.9</PersonalOID>
|
||||
<PersonalKood>D07907</PersonalKood>
|
||||
<PersonalPerekonnaNimi>Eduard</PersonalPerekonnaNimi>
|
||||
<PersonalEesNimi>Tsvetkov</PersonalEesNimi>
|
||||
<Telefon>+37258131202</Telefon>
|
||||
</Personal>`;
|
||||
};
|
||||
|
||||
export const getPatient = ({
|
||||
idCode,
|
||||
|
||||
@@ -2,15 +2,14 @@ import { z } from 'zod';
|
||||
|
||||
export const companyOfferSchema = z.object({
|
||||
companyName: z.string({
|
||||
required_error: 'Company name is required',
|
||||
error: 'Company name is required',
|
||||
}),
|
||||
contactPerson: z.string({
|
||||
required_error: 'Contact person is required',
|
||||
error: 'Contact person is required',
|
||||
}),
|
||||
email: z
|
||||
.string({
|
||||
required_error: 'Email is required',
|
||||
})
|
||||
.email('Invalid email'),
|
||||
.email({
|
||||
error: 'Invalid email',
|
||||
}),
|
||||
phone: z.string().optional(),
|
||||
});
|
||||
|
||||
@@ -50,7 +50,7 @@ const config = {
|
||||
},
|
||||
experimental: {
|
||||
mdxRs: true,
|
||||
reactCompiler: ENABLE_REACT_COMPILER,
|
||||
reactCompiler: false,
|
||||
optimizePackageImports: [
|
||||
'recharts',
|
||||
'lucide-react',
|
||||
|
||||
@@ -33,13 +33,13 @@
|
||||
"@hookform/resolvers": "^5.1.1",
|
||||
"@kit/accounts": "workspace:*",
|
||||
"@kit/admin": "workspace:*",
|
||||
"@kit/doctor": "workspace:*",
|
||||
"@kit/analytics": "workspace:*",
|
||||
"@kit/auth": "workspace:*",
|
||||
"@kit/billing": "workspace:*",
|
||||
"@kit/billing-gateway": "workspace:*",
|
||||
"@kit/cms": "workspace:*",
|
||||
"@kit/database-webhooks": "workspace:*",
|
||||
"@kit/doctor": "workspace:*",
|
||||
"@kit/email-templates": "workspace:*",
|
||||
"@kit/i18n": "workspace:*",
|
||||
"@kit/mailers": "workspace:*",
|
||||
@@ -82,7 +82,7 @@
|
||||
"sonner": "^2.0.5",
|
||||
"tailwind-merge": "^3.3.1",
|
||||
"ts-node": "^10.9.2",
|
||||
"zod": "^3.25.67"
|
||||
"zod": "^4.1.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@hookform/resolvers": "^5.0.1",
|
||||
|
||||
@@ -21,39 +21,25 @@ export const PaymentTypeSchema = z.enum(['one-time', 'recurring']);
|
||||
export const LineItemSchema = z
|
||||
.object({
|
||||
id: z
|
||||
.string({
|
||||
description:
|
||||
'Unique identifier for the line item. Defined by the Provider.',
|
||||
})
|
||||
.string()
|
||||
.describe('Unique identifier for the line item. Defined by the Provider.')
|
||||
.min(1),
|
||||
name: z
|
||||
.string({
|
||||
description: 'Name of the line item. Displayed to the user.',
|
||||
})
|
||||
.string().describe('Name of the line item. Displayed to the user.')
|
||||
.min(1),
|
||||
description: z
|
||||
.string({
|
||||
description:
|
||||
'Description of the line item. Displayed to the user and will replace the auto-generated description inferred' +
|
||||
' from the line item. This is useful if you want to provide a more detailed description to the user.',
|
||||
})
|
||||
.string().describe('Description of the line item. Displayed to the user and will replace the auto-generated description inferred' +
|
||||
' from the line item. This is useful if you want to provide a more detailed description to the user.')
|
||||
.optional(),
|
||||
cost: z
|
||||
.number({
|
||||
description: 'Cost of the line item. Displayed to the user.',
|
||||
})
|
||||
.number().describe('Cost of the line item. Displayed to the user.')
|
||||
.min(0),
|
||||
type: LineItemTypeSchema,
|
||||
unit: z
|
||||
.string({
|
||||
description:
|
||||
'Unit of the line item. Displayed to the user. Example "seat" or "GB"',
|
||||
})
|
||||
.string().describe('Unit of the line item. Displayed to the user. Example "seat" or "GB"')
|
||||
.optional(),
|
||||
setupFee: z
|
||||
.number({
|
||||
description: `Lemon Squeezy only: If true, in addition to the cost, a setup fee will be charged.`,
|
||||
})
|
||||
.number().describe(`Lemon Squeezy only: If true, in addition to the cost, a setup fee will be charged.`)
|
||||
.positive()
|
||||
.optional(),
|
||||
tiers: z
|
||||
@@ -92,14 +78,10 @@ export const LineItemSchema = z
|
||||
export const PlanSchema = z
|
||||
.object({
|
||||
id: z
|
||||
.string({
|
||||
description: 'Unique identifier for the plan. Defined by yourself.',
|
||||
})
|
||||
.string().describe('Unique identifier for the plan. Defined by yourself.')
|
||||
.min(1),
|
||||
name: z
|
||||
.string({
|
||||
description: 'Name of the plan. Displayed to the user.',
|
||||
})
|
||||
.string().describe('Name of the plan. Displayed to the user.')
|
||||
.min(1),
|
||||
interval: BillingIntervalSchema.optional(),
|
||||
custom: z.boolean().default(false).optional(),
|
||||
@@ -124,10 +106,7 @@ export const PlanSchema = z
|
||||
},
|
||||
),
|
||||
trialDays: z
|
||||
.number({
|
||||
description:
|
||||
'Number of days for the trial period. Leave empty for no trial.',
|
||||
})
|
||||
.number().describe('Number of days for the trial period. Leave empty for no trial.')
|
||||
.positive()
|
||||
.optional(),
|
||||
paymentType: PaymentTypeSchema,
|
||||
@@ -209,54 +188,34 @@ export const PlanSchema = z
|
||||
const ProductSchema = z
|
||||
.object({
|
||||
id: z
|
||||
.string({
|
||||
description:
|
||||
'Unique identifier for the product. Defined by th Provider.',
|
||||
})
|
||||
.string().describe('Unique identifier for the product. Defined by th Provider.')
|
||||
.min(1),
|
||||
name: z
|
||||
.string({
|
||||
description: 'Name of the product. Displayed to the user.',
|
||||
})
|
||||
.string().describe('Name of the product. Displayed to the user.')
|
||||
.min(1),
|
||||
description: z
|
||||
.string({
|
||||
description: 'Description of the product. Displayed to the user.',
|
||||
})
|
||||
.string().describe('Description of the product. Displayed to the user.')
|
||||
.min(1),
|
||||
currency: z
|
||||
.string({
|
||||
description: 'Currency code for the product. Displayed to the user.',
|
||||
})
|
||||
.string().describe('Currency code for the product. Displayed to the user.')
|
||||
.min(3)
|
||||
.max(3),
|
||||
badge: z
|
||||
.string({
|
||||
description:
|
||||
'Badge for the product. Displayed to the user. Example: "Popular"',
|
||||
})
|
||||
.string().describe('Badge for the product. Displayed to the user. Example: "Popular"')
|
||||
.optional(),
|
||||
features: z
|
||||
.array(
|
||||
z.string({
|
||||
description: 'Features of the product. Displayed to the user.',
|
||||
}),
|
||||
)
|
||||
z.string(),
|
||||
).describe('Features of the product. Displayed to the user.')
|
||||
.nonempty(),
|
||||
enableDiscountField: z
|
||||
.boolean({
|
||||
description: 'Enable discount field for the product in the checkout.',
|
||||
})
|
||||
.boolean().describe('Enable discount field for the product in the checkout.')
|
||||
.optional(),
|
||||
highlighted: z
|
||||
.boolean({
|
||||
description: 'Highlight this product. Displayed to the user.',
|
||||
})
|
||||
.boolean().describe('Highlight this product. Displayed to the user.')
|
||||
.optional(),
|
||||
hidden: z
|
||||
.boolean({
|
||||
description: 'Hide this product from being displayed to users.',
|
||||
})
|
||||
.boolean().describe('Hide this product from being displayed to users.')
|
||||
.optional(),
|
||||
plans: z.array(PlanSchema),
|
||||
})
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const ReportBillingUsageSchema = z.object({
|
||||
id: z.string({
|
||||
description:
|
||||
'The id of the usage record. For Stripe a customer ID, for LS a subscription item ID.',
|
||||
}),
|
||||
id: z.string().describe('The id of the usage record. For Stripe a customer ID, for LS a subscription item ID.'),
|
||||
eventName: z
|
||||
.string({
|
||||
description: 'The name of the event that triggered the usage',
|
||||
})
|
||||
.string()
|
||||
.describe('The name of the event that triggered the usage')
|
||||
.optional(),
|
||||
usage: z.object({
|
||||
quantity: z.number(),
|
||||
|
||||
@@ -3,8 +3,8 @@ import { z } from 'zod';
|
||||
export const UpdateHealthBenefitSchema = z.object({
|
||||
occurance: z
|
||||
.string({
|
||||
required_error: 'Occurance is required',
|
||||
error: 'Occurance is required',
|
||||
})
|
||||
.nonempty(),
|
||||
amount: z.number({ required_error: 'Amount is required' }),
|
||||
amount: z.number({ error: 'Amount is required' }),
|
||||
});
|
||||
|
||||
@@ -4,12 +4,12 @@ export const MontonioServerEnvSchema = z
|
||||
.object({
|
||||
secretKey: z
|
||||
.string({
|
||||
required_error: `Please provide the variable MONTONIO_SECRET_KEY`,
|
||||
error: `Please provide the variable MONTONIO_SECRET_KEY`,
|
||||
})
|
||||
.min(1),
|
||||
apiUrl: z
|
||||
.string({
|
||||
required_error: `Please provide the variable MONTONIO_API_URL`,
|
||||
error: `Please provide the variable MONTONIO_API_URL`,
|
||||
})
|
||||
.min(1),
|
||||
});
|
||||
|
||||
@@ -4,12 +4,12 @@ export const StripeServerEnvSchema = z
|
||||
.object({
|
||||
secretKey: z
|
||||
.string({
|
||||
required_error: `Please provide the variable STRIPE_SECRET_KEY`,
|
||||
error: `Please provide the variable STRIPE_SECRET_KEY`,
|
||||
})
|
||||
.min(1),
|
||||
webhooksSecret: z
|
||||
.string({
|
||||
required_error: `Please provide the variable STRIPE_WEBHOOK_SECRET`,
|
||||
error: `Please provide the variable STRIPE_WEBHOOK_SECRET`,
|
||||
})
|
||||
.min(1),
|
||||
})
|
||||
|
||||
@@ -4,9 +4,9 @@ import { DatabaseWebhookVerifierService } from './database-webhook-verifier.serv
|
||||
|
||||
const webhooksSecret = z
|
||||
.string({
|
||||
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.`,
|
||||
error: `Provide the variable SUPABASE_DB_WEBHOOK_SECRET. This is used to authenticate the webhook event from Supabase.`,
|
||||
})
|
||||
.describe(`The secret used to verify the webhook signature`,)
|
||||
.min(1)
|
||||
.parse(process.env.SUPABASE_DB_WEBHOOK_SECRET);
|
||||
|
||||
|
||||
@@ -1 +1,3 @@
|
||||
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'
|
||||
|
||||
@@ -101,14 +101,14 @@ export function PersonalAccountDropdown({
|
||||
personalAccountData?.application_role === ApplicationRoleEnum.SuperAdmin;
|
||||
|
||||
return hasAdminRole && hasTotpFactor;
|
||||
}, [user, personalAccountData, hasTotpFactor]);
|
||||
}, [personalAccountData, hasTotpFactor]);
|
||||
|
||||
const isDoctor = useMemo(() => {
|
||||
const hasDoctorRole =
|
||||
personalAccountData?.application_role === ApplicationRoleEnum.Doctor;
|
||||
|
||||
return hasDoctorRole && hasTotpFactor;
|
||||
}, [user, personalAccountData, hasTotpFactor]);
|
||||
}, [personalAccountData, hasTotpFactor]);
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
|
||||
@@ -1,208 +0,0 @@
|
||||
'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;
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import { toast } from 'sonner';
|
||||
|
||||
import { useFetchAuthFactors } from '@kit/supabase/hooks/use-fetch-mfa-factors';
|
||||
import { useSupabase } from '@kit/supabase/hooks/use-supabase';
|
||||
import { useUser } from '@kit/supabase/hooks/use-user';
|
||||
import { useFactorsMutationKey } from '@kit/supabase/hooks/use-user-factors-mutation-key';
|
||||
import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert';
|
||||
import {
|
||||
@@ -46,13 +47,18 @@ import { Trans } from '@kit/ui/trans';
|
||||
|
||||
import { MultiFactorAuthSetupDialog } from './multi-factor-auth-setup-dialog';
|
||||
|
||||
export function MultiFactorAuthFactorsList(props: { userId: string }) {
|
||||
export function MultiFactorAuthFactorsList() {
|
||||
const { data: user } = useUser();
|
||||
if (!user?.id) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={'flex flex-col space-y-4'}>
|
||||
<FactorsTableContainer userId={props.userId} />
|
||||
<FactorsTableContainer userId={user?.id} />
|
||||
|
||||
<div>
|
||||
<MultiFactorAuthSetupDialog userId={props.userId} />
|
||||
<MultiFactorAuthSetupDialog userId={user?.id} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
'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>
|
||||
);
|
||||
}
|
||||
@@ -1,206 +0,0 @@
|
||||
'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>
|
||||
);
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
'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)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,98 +0,0 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -1,168 +0,0 @@
|
||||
'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}`;
|
||||
}
|
||||
@@ -2,19 +2,19 @@ import { SupabaseClient } from '@supabase/supabase-js';
|
||||
|
||||
import { Database } from '@kit/supabase/database';
|
||||
|
||||
import {
|
||||
AnalysisResultDetails,
|
||||
UserAnalysis,
|
||||
UserAnalysisResponse,
|
||||
} from '../types/accounts';
|
||||
import { AnalysisResultDetails, UserAnalysis } from '../types/accounts';
|
||||
|
||||
export type AccountWithParams =
|
||||
Database['medreport']['Tables']['accounts']['Row'] & {
|
||||
account_params:
|
||||
| Pick<
|
||||
accountParams:
|
||||
| (Pick<
|
||||
Database['medreport']['Tables']['account_params']['Row'],
|
||||
'weight' | 'height'
|
||||
>[]
|
||||
> & {
|
||||
isSmoker:
|
||||
| Database['medreport']['Tables']['account_params']['Row']['is_smoker']
|
||||
| null;
|
||||
})
|
||||
| null;
|
||||
};
|
||||
|
||||
@@ -35,7 +35,9 @@ class AccountsApi {
|
||||
const { data, error } = await this.client
|
||||
.schema('medreport')
|
||||
.from('accounts')
|
||||
.select('*, account_params: account_params (weight, height)')
|
||||
.select(
|
||||
'*, accountParams: account_params (weight, height, isSmoker:is_smoker)',
|
||||
)
|
||||
.eq('id', id)
|
||||
.single();
|
||||
|
||||
|
||||
@@ -32,9 +32,8 @@ const SPECIAL_CHARACTERS_REGEX = /[!@#$%^&*()+=[\]{};':"\\|,.<>/?]/;
|
||||
* @name CompanyNameSchema
|
||||
*/
|
||||
export const CompanyNameSchema = z
|
||||
.string({
|
||||
description: 'The name of the company account',
|
||||
})
|
||||
.string()
|
||||
.describe('The name of the company account')
|
||||
.min(2)
|
||||
.max(50)
|
||||
.refine(
|
||||
|
||||
@@ -410,7 +410,7 @@ export async function getAnalysisResultsForDoctor(
|
||||
.from('accounts')
|
||||
.select(
|
||||
`primary_owner_user_id, id, name, last_name, personal_code, phone, email, preferred_locale,
|
||||
account_params(height,weight)`,
|
||||
accountParams:account_params(height,weight)`,
|
||||
)
|
||||
.eq('is_personal_account', true)
|
||||
.eq('primary_owner_user_id', userId)
|
||||
@@ -472,7 +472,7 @@ export async function getAnalysisResultsForDoctor(
|
||||
last_name,
|
||||
personal_code,
|
||||
phone,
|
||||
account_params,
|
||||
accountParams,
|
||||
preferred_locale,
|
||||
} = accountWithParams[0];
|
||||
|
||||
@@ -513,8 +513,8 @@ export async function getAnalysisResultsForDoctor(
|
||||
personalCode: personal_code,
|
||||
phone,
|
||||
email,
|
||||
height: account_params?.[0]?.height,
|
||||
weight: account_params?.[0]?.weight,
|
||||
height: accountParams?.height,
|
||||
weight: accountParams?.weight,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -87,7 +87,10 @@ export async function getOrSetCart(countryCode: string) {
|
||||
return cart;
|
||||
}
|
||||
|
||||
export async function updateCart({ id, ...data }: HttpTypes.StoreUpdateCart & { id?: string }) {
|
||||
export async function updateCart(
|
||||
{ id, ...data }: HttpTypes.StoreUpdateCart & { id?: string },
|
||||
{ onSuccess, onError }: { onSuccess: () => void, onError: () => void } = { onSuccess: () => {}, onError: () => {} },
|
||||
) {
|
||||
const cartId = id || (await getCartId());
|
||||
|
||||
if (!cartId) {
|
||||
@@ -109,9 +112,13 @@ export async function updateCart({ id, ...data }: HttpTypes.StoreUpdateCart & {
|
||||
const fulfillmentCacheTag = await getCacheTag("fulfillment");
|
||||
revalidateTag(fulfillmentCacheTag);
|
||||
|
||||
onSuccess();
|
||||
return cart;
|
||||
})
|
||||
.catch(medusaError);
|
||||
.catch((e) => {
|
||||
onError();
|
||||
return medusaError(e);
|
||||
});
|
||||
}
|
||||
|
||||
export async function addToCart({
|
||||
@@ -259,7 +266,10 @@ export async function initiatePaymentSession(
|
||||
.catch(medusaError);
|
||||
}
|
||||
|
||||
export async function applyPromotions(codes: string[]) {
|
||||
export async function applyPromotions(
|
||||
codes: string[],
|
||||
{ onSuccess, onError }: { onSuccess: () => void, onError: () => void } = { onSuccess: () => {}, onError: () => {} },
|
||||
) {
|
||||
const cartId = await getCartId();
|
||||
|
||||
if (!cartId) {
|
||||
@@ -278,8 +288,13 @@ export async function applyPromotions(codes: string[]) {
|
||||
|
||||
const fulfillmentCacheTag = await getCacheTag("fulfillment");
|
||||
revalidateTag(fulfillmentCacheTag);
|
||||
|
||||
onSuccess();
|
||||
})
|
||||
.catch(medusaError);
|
||||
.catch((e) => {
|
||||
onError();
|
||||
return medusaError(e);
|
||||
});
|
||||
}
|
||||
|
||||
export async function applyGiftCard(code: string) {
|
||||
@@ -427,7 +442,7 @@ export async function placeOrder(cartId?: string, options: { revalidateCacheTags
|
||||
} else {
|
||||
throw new Error("Cart is not an order");
|
||||
}
|
||||
|
||||
|
||||
return retrieveOrder(cartRes.order.id);
|
||||
}
|
||||
|
||||
|
||||
@@ -127,7 +127,7 @@ export async function login(_currentState: unknown, formData: FormData) {
|
||||
}
|
||||
}
|
||||
|
||||
export async function signout(countryCode: string) {
|
||||
export async function signout(countryCode?: string, shouldRedirect = true) {
|
||||
await sdk.auth.logout()
|
||||
|
||||
await removeAuthToken()
|
||||
@@ -140,7 +140,9 @@ export async function signout(countryCode: string) {
|
||||
const cartCacheTag = await getCacheTag("carts")
|
||||
revalidateTag(cartCacheTag)
|
||||
|
||||
redirect(`/${countryCode}/account`)
|
||||
if (shouldRedirect) {
|
||||
redirect(`/${countryCode!}/account`)
|
||||
}
|
||||
}
|
||||
|
||||
export async function transferCart() {
|
||||
@@ -272,7 +274,12 @@ export async function medusaLoginOrRegister(credentials: {
|
||||
password,
|
||||
});
|
||||
await setAuthToken(token as string);
|
||||
await transferCart();
|
||||
|
||||
try {
|
||||
await transferCart();
|
||||
} catch (e) {
|
||||
console.error("Failed to transfer cart", e);
|
||||
}
|
||||
|
||||
const customerCacheTag = await getCacheTag("customers");
|
||||
revalidateTag(customerCacheTag);
|
||||
@@ -307,7 +314,12 @@ export async function medusaLoginOrRegister(credentials: {
|
||||
|
||||
const customerCacheTag = await getCacheTag("customers");
|
||||
revalidateTag(customerCacheTag);
|
||||
await transferCart();
|
||||
|
||||
try {
|
||||
await transferCart();
|
||||
} catch (e) {
|
||||
console.error("Failed to transfer cart", e);
|
||||
}
|
||||
|
||||
const customer = await retrieveCustomer();
|
||||
if (!customer) {
|
||||
|
||||
@@ -14,7 +14,7 @@ export const listProducts = async ({
|
||||
regionId,
|
||||
}: {
|
||||
pageParam?: number
|
||||
queryParams?: HttpTypes.FindParams & HttpTypes.StoreProductParams & { "type_id[0]"?: string; id?: string[] }
|
||||
queryParams?: HttpTypes.FindParams & HttpTypes.StoreProductParams & { "type_id[0]"?: string; id?: string[], category_id?: string }
|
||||
countryCode?: string
|
||||
regionId?: string
|
||||
}): Promise<{
|
||||
@@ -63,7 +63,7 @@ export const listProducts = async ({
|
||||
offset,
|
||||
region_id: region?.id,
|
||||
fields:
|
||||
"*variants.calculated_price,+variants.inventory_quantity,+metadata,+tags",
|
||||
"*variants.calculated_price,+variants.inventory_quantity,+metadata,+tags,+status",
|
||||
...queryParams,
|
||||
},
|
||||
headers,
|
||||
|
||||
@@ -17,22 +17,22 @@ const env = z
|
||||
.object({
|
||||
invitePath: z
|
||||
.string({
|
||||
required_error: 'The property invitePath is required',
|
||||
error: 'The property invitePath is required',
|
||||
})
|
||||
.min(1),
|
||||
siteURL: z
|
||||
.string({
|
||||
required_error: 'NEXT_PUBLIC_SITE_URL is required',
|
||||
error: 'NEXT_PUBLIC_SITE_URL is required',
|
||||
})
|
||||
.min(1),
|
||||
productName: z
|
||||
.string({
|
||||
required_error: 'NEXT_PUBLIC_PRODUCT_NAME is required',
|
||||
error: 'NEXT_PUBLIC_PRODUCT_NAME is required',
|
||||
})
|
||||
.min(1),
|
||||
emailSender: z
|
||||
.string({
|
||||
required_error: 'EMAIL_SENDER is required',
|
||||
error: 'EMAIL_SENDER is required',
|
||||
})
|
||||
.min(1),
|
||||
})
|
||||
|
||||
@@ -78,7 +78,7 @@ class AccountWebhooksService {
|
||||
productName: z.string(),
|
||||
fromEmail: z
|
||||
.string({
|
||||
required_error: 'EMAIL_SENDER is required',
|
||||
error: 'EMAIL_SENDER is required',
|
||||
})
|
||||
.min(1),
|
||||
})
|
||||
|
||||
@@ -8,9 +8,9 @@ type Config = z.infer<typeof MailerSchema>;
|
||||
|
||||
const RESEND_API_KEY = z
|
||||
.string({
|
||||
description: 'The API key for the Resend API',
|
||||
required_error: 'Please provide the API key for the Resend API',
|
||||
error: 'Please provide the API key for the Resend API',
|
||||
})
|
||||
.describe('The API key for the Resend API')
|
||||
.parse(process.env.RESEND_API_KEY);
|
||||
|
||||
export function createResendMailer() {
|
||||
|
||||
@@ -4,25 +4,19 @@ import { z } from 'zod';
|
||||
|
||||
export const SmtpConfigSchema = z.object({
|
||||
user: z.string({
|
||||
description:
|
||||
'This is the email account to send emails from. This is specific to the email provider.',
|
||||
required_error: `Please provide the variable EMAIL_USER`,
|
||||
}),
|
||||
error: `Please provide the variable EMAIL_USER`,
|
||||
})
|
||||
.describe('This is the email account to send emails from. This is specific to the email provider.'),
|
||||
pass: z.string({
|
||||
description: 'This is the password for the email account',
|
||||
required_error: `Please provide the variable EMAIL_PASSWORD`,
|
||||
}),
|
||||
error: `Please provide the variable EMAIL_PASSWORD`,
|
||||
}).describe('This is the password for the email account'),
|
||||
host: z.string({
|
||||
description: 'This is the SMTP host for the email provider',
|
||||
required_error: `Please provide the variable EMAIL_HOST`,
|
||||
}),
|
||||
error: `Please provide the variable EMAIL_HOST`,
|
||||
}).describe('This is the SMTP host for the email provider'),
|
||||
port: z.number({
|
||||
description:
|
||||
'This is the port for the email provider. Normally 587 or 465.',
|
||||
required_error: `Please provide the variable EMAIL_PORT`,
|
||||
}),
|
||||
error: `Please provide the variable EMAIL_PORT`,
|
||||
}).describe('This is the port for the email provider. Normally 587 or 465.'),
|
||||
secure: z.boolean({
|
||||
description: 'This is whether the connection is secure or not',
|
||||
required_error: `Please provide the variable EMAIL_TLS`,
|
||||
}),
|
||||
error: `Please provide the variable EMAIL_TLS`,
|
||||
}).describe('This is whether the connection is secure or not'),
|
||||
});
|
||||
|
||||
@@ -4,9 +4,9 @@ import { MonitoringService } from '@kit/monitoring-core';
|
||||
|
||||
const apiKey = z
|
||||
.string({
|
||||
required_error: 'NEXT_PUBLIC_BASELIME_KEY is required',
|
||||
description: 'The Baseline API key',
|
||||
error: 'NEXT_PUBLIC_BASELIME_KEY is required',
|
||||
})
|
||||
.describe('The Baseline API key')
|
||||
.parse(process.env.NEXT_PUBLIC_BASELIME_KEY);
|
||||
|
||||
export class BaselimeServerMonitoringService implements MonitoringService {
|
||||
|
||||
@@ -6,14 +6,14 @@ import { getLogger } from '@kit/shared/logger';
|
||||
|
||||
const EMAIL_SENDER = z
|
||||
.string({
|
||||
required_error: 'EMAIL_SENDER is required',
|
||||
error: 'EMAIL_SENDER is required',
|
||||
})
|
||||
.min(1)
|
||||
.parse(process.env.EMAIL_SENDER);
|
||||
|
||||
const PRODUCT_NAME = z
|
||||
.string({
|
||||
required_error: 'PRODUCT_NAME is required',
|
||||
error: 'PRODUCT_NAME is required',
|
||||
})
|
||||
.min(1)
|
||||
.parse(process.env.NEXT_PUBLIC_PRODUCT_NAME);
|
||||
|
||||
@@ -4,12 +4,10 @@ import type { User } from '@supabase/supabase-js';
|
||||
|
||||
import { PersonalAccountDropdown } from '@kit/accounts/personal-account-dropdown';
|
||||
import { ApplicationRole } from '@kit/accounts/types/accounts';
|
||||
import { featureFlagsConfig, pathsConfig } from '@kit/shared/config';
|
||||
import { useSignOut } from '@kit/supabase/hooks/use-sign-out';
|
||||
import { useUser } from '@kit/supabase/hooks/use-user';
|
||||
|
||||
import { pathsConfig, featureFlagsConfig } from '@kit/shared/config';
|
||||
|
||||
|
||||
const paths = {
|
||||
home: pathsConfig.app.home,
|
||||
admin: pathsConfig.app.admin,
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
import { Trans } from '@kit/ui/trans';
|
||||
import { ButtonTooltip } from './ui/button-tooltip';
|
||||
import { PackageHeader } from './package-header';
|
||||
import { pathsConfig } from '../config';
|
||||
|
||||
export type AnalysisPackageWithVariant = Pick<StoreProduct, 'title' | 'description' | 'subtitle' | 'metadata'> & {
|
||||
variantId: string;
|
||||
@@ -57,7 +58,7 @@ export default function SelectAnalysisPackage({
|
||||
});
|
||||
setIsAddingToCart(false);
|
||||
toast.success(<Trans i18nKey={'order-analysis-package:analysisPackageAddedToCart'} />);
|
||||
router.push('/home/cart');
|
||||
router.push(pathsConfig.app.cart);
|
||||
} catch (e) {
|
||||
toast.error(<Trans i18nKey={'order-analysis-package:analysisPackageAddToCartError'} />);
|
||||
setIsAddingToCart(false);
|
||||
|
||||
24
packages/shared/src/components/sign-out-dropdown-item.tsx
Normal file
24
packages/shared/src/components/sign-out-dropdown-item.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
'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>
|
||||
);
|
||||
}
|
||||
33
packages/shared/src/components/ui/dropdown-link.tsx
Normal file
33
packages/shared/src/components/ui/dropdown-link.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
'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>
|
||||
);
|
||||
}
|
||||
@@ -13,7 +13,7 @@ export function InfoTooltip({
|
||||
content,
|
||||
icon,
|
||||
}: {
|
||||
content?: string | null;
|
||||
content?: JSX.Element | string | null;
|
||||
icon?: JSX.Element;
|
||||
}) {
|
||||
if (!content) return null;
|
||||
@@ -23,7 +23,7 @@ export function InfoTooltip({
|
||||
<TooltipTrigger>
|
||||
{icon || <Info className="size-4 cursor-pointer" />}
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{content}</TooltipContent>
|
||||
<TooltipContent className='sm:max-w-[400px]'>{content}</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
);
|
||||
|
||||
@@ -6,32 +6,30 @@ const AppConfigSchema = z
|
||||
.object({
|
||||
name: z
|
||||
.string({
|
||||
description: `This is the name of your SaaS. Ex. "Makerkit"`,
|
||||
required_error: `Please provide the variable NEXT_PUBLIC_PRODUCT_NAME`,
|
||||
error: `Please provide the variable NEXT_PUBLIC_PRODUCT_NAME`,
|
||||
})
|
||||
.describe(`This is the name of your SaaS. Ex. "Makerkit"`)
|
||||
.min(1),
|
||||
title: z
|
||||
.string({
|
||||
description: `This is the default title tag of your SaaS.`,
|
||||
required_error: `Please provide the variable NEXT_PUBLIC_SITE_TITLE`,
|
||||
error: `Please provide the variable NEXT_PUBLIC_SITE_TITLE`,
|
||||
})
|
||||
.describe(`This is the default title tag of your SaaS.`)
|
||||
.min(1),
|
||||
description: z.string({
|
||||
description: `This is the default description of your SaaS.`,
|
||||
required_error: `Please provide the variable NEXT_PUBLIC_SITE_DESCRIPTION`,
|
||||
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
|
||||
.string({
|
||||
description: `This is the default locale of your SaaS.`,
|
||||
required_error: `Please provide the variable NEXT_PUBLIC_DEFAULT_LOCALE`,
|
||||
error: `Please provide the variable NEXT_PUBLIC_DEFAULT_LOCALE`,
|
||||
})
|
||||
.describe(`This is the default locale of your SaaS.`)
|
||||
.default('en'),
|
||||
theme: z.enum(['light', 'dark', 'system']),
|
||||
production: z.boolean(),
|
||||
|
||||
@@ -6,22 +6,14 @@ const providers: z.ZodType<Provider> = getProviders();
|
||||
|
||||
const AuthConfigSchema = z.object({
|
||||
captchaTokenSiteKey: z
|
||||
.string({
|
||||
description: 'The reCAPTCHA site key.',
|
||||
})
|
||||
.string().describe('The reCAPTCHA site key.')
|
||||
.optional(),
|
||||
displayTermsCheckbox: z
|
||||
.boolean({
|
||||
description: 'Whether to display the terms checkbox during sign-up.',
|
||||
})
|
||||
.boolean().describe('Whether to display the terms checkbox during sign-up.')
|
||||
.optional(),
|
||||
providers: z.object({
|
||||
password: z.boolean({
|
||||
description: 'Enable password authentication.',
|
||||
}),
|
||||
magicLink: z.boolean({
|
||||
description: 'Enable magic link authentication.',
|
||||
}),
|
||||
password: z.boolean().describe('Enable password authentication.'),
|
||||
magicLink: z.boolean().describe('Enable magic link authentication.'),
|
||||
oAuth: providers.array(),
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -4,56 +4,56 @@ type LanguagePriority = 'user' | 'application';
|
||||
|
||||
const FeatureFlagsSchema = z.object({
|
||||
enableThemeToggle: z.boolean({
|
||||
description: 'Enable theme toggle in the user interface.',
|
||||
required_error: 'Provide the variable NEXT_PUBLIC_ENABLE_THEME_TOGGLE',
|
||||
}),
|
||||
error: 'Provide the variable NEXT_PUBLIC_ENABLE_THEME_TOGGLE',
|
||||
})
|
||||
.describe( 'Enable theme toggle in the user interface.'),
|
||||
enableAccountDeletion: z.boolean({
|
||||
description: 'Enable personal account deletion.',
|
||||
required_error:
|
||||
error:
|
||||
'Provide the variable NEXT_PUBLIC_ENABLE_PERSONAL_ACCOUNT_DELETION',
|
||||
}),
|
||||
})
|
||||
.describe('Enable personal account deletion.'),
|
||||
enableTeamDeletion: z.boolean({
|
||||
description: 'Enable team deletion.',
|
||||
required_error:
|
||||
error:
|
||||
'Provide the variable NEXT_PUBLIC_ENABLE_TEAM_ACCOUNTS_DELETION',
|
||||
}),
|
||||
})
|
||||
.describe('Enable team deletion.'),
|
||||
enableTeamAccounts: z.boolean({
|
||||
description: 'Enable team accounts.',
|
||||
required_error: 'Provide the variable NEXT_PUBLIC_ENABLE_TEAM_ACCOUNTS',
|
||||
}),
|
||||
error: 'Provide the variable NEXT_PUBLIC_ENABLE_TEAM_ACCOUNTS',
|
||||
})
|
||||
.describe('Enable team accounts.'),
|
||||
enableTeamCreation: z.boolean({
|
||||
description: 'Enable team creation.',
|
||||
required_error:
|
||||
error:
|
||||
'Provide the variable NEXT_PUBLIC_ENABLE_TEAM_ACCOUNTS_CREATION',
|
||||
}),
|
||||
})
|
||||
.describe('Enable team creation.'),
|
||||
enablePersonalAccountBilling: z.boolean({
|
||||
description: 'Enable personal account billing.',
|
||||
required_error:
|
||||
error:
|
||||
'Provide the variable NEXT_PUBLIC_ENABLE_PERSONAL_ACCOUNT_BILLING',
|
||||
}),
|
||||
})
|
||||
.describe('Enable personal account billing.'),
|
||||
enableTeamAccountBilling: z.boolean({
|
||||
description: 'Enable team account billing.',
|
||||
required_error:
|
||||
error:
|
||||
'Provide the variable NEXT_PUBLIC_ENABLE_TEAM_ACCOUNTS_BILLING',
|
||||
}),
|
||||
})
|
||||
.describe('Enable team account billing.'),
|
||||
languagePriority: z
|
||||
.enum(['user', 'application'], {
|
||||
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.`,
|
||||
error: 'Provide the variable NEXT_PUBLIC_LANGUAGE_PRIORITY',
|
||||
})
|
||||
.describe(`If set to user, use the user's preferred language. If set to application, use the application's default language.`)
|
||||
.default('application'),
|
||||
enableNotifications: z.boolean({
|
||||
description: 'Enable notifications functionality',
|
||||
required_error: 'Provide the variable NEXT_PUBLIC_ENABLE_NOTIFICATIONS',
|
||||
}),
|
||||
error: 'Provide the variable NEXT_PUBLIC_ENABLE_NOTIFICATIONS',
|
||||
})
|
||||
.describe('Enable notifications functionality'),
|
||||
realtimeNotifications: z.boolean({
|
||||
description: 'Enable realtime for the notifications functionality',
|
||||
required_error: 'Provide the variable NEXT_PUBLIC_REALTIME_NOTIFICATIONS',
|
||||
}),
|
||||
error: 'Provide the variable NEXT_PUBLIC_REALTIME_NOTIFICATIONS',
|
||||
})
|
||||
.describe('Enable realtime for the notifications functionality'),
|
||||
enableVersionUpdater: z.boolean({
|
||||
description: 'Enable version updater',
|
||||
required_error: 'Provide the variable NEXT_PUBLIC_ENABLE_VERSION_UPDATER',
|
||||
}),
|
||||
error: 'Provide the variable NEXT_PUBLIC_ENABLE_VERSION_UPDATER',
|
||||
})
|
||||
.describe('Enable version updater'),
|
||||
});
|
||||
|
||||
const featureFlagsConfig = FeatureFlagsSchema.parse({
|
||||
|
||||
@@ -14,6 +14,7 @@ const PathsSchema = z.object({
|
||||
}),
|
||||
app: z.object({
|
||||
home: z.string().min(1),
|
||||
cart: z.string().min(1),
|
||||
selectPackage: z.string().min(1),
|
||||
booking: z.string().min(1),
|
||||
bookingHandle: z.string().min(1),
|
||||
@@ -23,6 +24,8 @@ const PathsSchema = z.object({
|
||||
orderAnalysis: z.string().min(1),
|
||||
orderHealthAnalysis: z.string().min(1),
|
||||
personalAccountSettings: z.string().min(1),
|
||||
personalAccountPreferences: z.string().min(1),
|
||||
personalAccountSecurity: z.string().min(1),
|
||||
personalAccountBilling: z.string().min(1),
|
||||
personalAccountBillingReturn: z.string().min(1),
|
||||
accountHome: z.string().min(1),
|
||||
@@ -54,7 +57,10 @@ const pathsConfig = PathsSchema.parse({
|
||||
},
|
||||
app: {
|
||||
home: '/home',
|
||||
cart: '/home/cart',
|
||||
personalAccountSettings: '/home/settings',
|
||||
personalAccountPreferences: '/home/settings/preferences',
|
||||
personalAccountSecurity: '/home/settings/security',
|
||||
personalAccountBilling: '/home/billing',
|
||||
personalAccountBillingReturn: '/home/billing/return',
|
||||
accountHome: '/home/[account]',
|
||||
|
||||
@@ -5,7 +5,6 @@ import {
|
||||
MousePointerClick,
|
||||
ShoppingCart,
|
||||
Stethoscope,
|
||||
TestTube2,
|
||||
} from 'lucide-react';
|
||||
import { z } from 'zod';
|
||||
|
||||
|
||||
@@ -199,7 +199,6 @@ export type Database = {
|
||||
changed_by: string
|
||||
created_at: string
|
||||
id: number
|
||||
extra_data?: Json | null
|
||||
}
|
||||
Insert: {
|
||||
account_id: string
|
||||
@@ -207,7 +206,6 @@ export type Database = {
|
||||
changed_by: string
|
||||
created_at?: string
|
||||
id?: number
|
||||
extra_data?: Json | null
|
||||
}
|
||||
Update: {
|
||||
account_id?: string
|
||||
@@ -215,7 +213,6 @@ export type Database = {
|
||||
changed_by?: string
|
||||
created_at?: string
|
||||
id?: number
|
||||
extra_data?: Json | null
|
||||
}
|
||||
Relationships: []
|
||||
}
|
||||
@@ -320,10 +317,10 @@ export type Database = {
|
||||
Functions: {
|
||||
graphql: {
|
||||
Args: {
|
||||
extensions?: Json
|
||||
operationName?: string
|
||||
query?: string
|
||||
variables?: Json
|
||||
extensions?: Json
|
||||
}
|
||||
Returns: Json
|
||||
}
|
||||
@@ -342,6 +339,7 @@ export type Database = {
|
||||
account_id: string
|
||||
height: number | null
|
||||
id: string
|
||||
is_smoker: boolean | null
|
||||
recorded_at: string
|
||||
weight: number | null
|
||||
}
|
||||
@@ -349,6 +347,7 @@ export type Database = {
|
||||
account_id?: string
|
||||
height?: number | null
|
||||
id?: string
|
||||
is_smoker?: boolean | null
|
||||
recorded_at?: string
|
||||
weight?: number | null
|
||||
}
|
||||
@@ -356,6 +355,7 @@ export type Database = {
|
||||
account_id?: string
|
||||
height?: number | null
|
||||
id?: string
|
||||
is_smoker?: boolean | null
|
||||
recorded_at?: string
|
||||
weight?: number | null
|
||||
}
|
||||
@@ -363,21 +363,21 @@ export type Database = {
|
||||
{
|
||||
foreignKeyName: "account_params_account_id_fkey"
|
||||
columns: ["account_id"]
|
||||
isOneToOne: false
|
||||
isOneToOne: true
|
||||
referencedRelation: "accounts"
|
||||
referencedColumns: ["id"]
|
||||
},
|
||||
{
|
||||
foreignKeyName: "account_params_account_id_fkey"
|
||||
columns: ["account_id"]
|
||||
isOneToOne: false
|
||||
isOneToOne: true
|
||||
referencedRelation: "user_account_workspace"
|
||||
referencedColumns: ["id"]
|
||||
},
|
||||
{
|
||||
foreignKeyName: "account_params_account_id_fkey"
|
||||
columns: ["account_id"]
|
||||
isOneToOne: false
|
||||
isOneToOne: true
|
||||
referencedRelation: "user_accounts"
|
||||
referencedColumns: ["id"]
|
||||
},
|
||||
@@ -1091,7 +1091,7 @@ export type Database = {
|
||||
price: number
|
||||
price_periods: string | null
|
||||
requires_payment: boolean
|
||||
sync_id: string | null
|
||||
sync_id: string
|
||||
updated_at: string | null
|
||||
}
|
||||
Insert: {
|
||||
@@ -1110,7 +1110,7 @@ export type Database = {
|
||||
price: number
|
||||
price_periods?: string | null
|
||||
requires_payment: boolean
|
||||
sync_id?: string | null
|
||||
sync_id: string
|
||||
updated_at?: string | null
|
||||
}
|
||||
Update: {
|
||||
@@ -1129,7 +1129,7 @@ export type Database = {
|
||||
price?: number
|
||||
price_periods?: string | null
|
||||
requires_payment?: boolean
|
||||
sync_id?: string | null
|
||||
sync_id?: string
|
||||
updated_at?: string | null
|
||||
}
|
||||
Relationships: [
|
||||
@@ -1150,7 +1150,7 @@ export type Database = {
|
||||
doctor_user_id: string | null
|
||||
id: number
|
||||
status: Database["medreport"]["Enums"]["analysis_feedback_status"]
|
||||
updated_at: string | null
|
||||
updated_at: string
|
||||
updated_by: string | null
|
||||
user_id: string
|
||||
value: string | null
|
||||
@@ -1162,7 +1162,7 @@ export type Database = {
|
||||
doctor_user_id?: string | null
|
||||
id?: number
|
||||
status?: Database["medreport"]["Enums"]["analysis_feedback_status"]
|
||||
updated_at?: string | null
|
||||
updated_at?: string
|
||||
updated_by?: string | null
|
||||
user_id: string
|
||||
value?: string | null
|
||||
@@ -1174,7 +1174,7 @@ export type Database = {
|
||||
doctor_user_id?: string | null
|
||||
id?: number
|
||||
status?: Database["medreport"]["Enums"]["analysis_feedback_status"]
|
||||
updated_at?: string | null
|
||||
updated_at?: string
|
||||
updated_by?: string | null
|
||||
user_id?: string
|
||||
value?: string | null
|
||||
@@ -1257,34 +1257,6 @@ export type Database = {
|
||||
},
|
||||
]
|
||||
}
|
||||
medipost_actions: {
|
||||
Row: {
|
||||
id: string
|
||||
action: string
|
||||
xml: string
|
||||
has_analysis_results: boolean
|
||||
created_at: string
|
||||
medusa_order_id: string
|
||||
response_xml: string
|
||||
has_error: boolean
|
||||
}
|
||||
Insert: {
|
||||
action: string
|
||||
xml: string
|
||||
has_analysis_results: boolean
|
||||
medusa_order_id: string
|
||||
response_xml: string
|
||||
has_error: boolean
|
||||
}
|
||||
Update: {
|
||||
action?: string
|
||||
xml?: string
|
||||
has_analysis_results?: boolean
|
||||
medusa_order_id?: string
|
||||
response_xml?: string
|
||||
has_error?: boolean
|
||||
}
|
||||
}
|
||||
medreport_product_groups: {
|
||||
Row: {
|
||||
created_at: string
|
||||
@@ -1871,17 +1843,19 @@ export type Database = {
|
||||
}
|
||||
create_nonce: {
|
||||
Args: {
|
||||
p_user_id?: string
|
||||
p_purpose?: string
|
||||
p_expires_in_seconds?: number
|
||||
p_metadata?: Json
|
||||
p_scopes?: string[]
|
||||
p_purpose?: string
|
||||
p_revoke_previous?: boolean
|
||||
p_scopes?: string[]
|
||||
p_user_id?: string
|
||||
}
|
||||
Returns: Json
|
||||
}
|
||||
create_team_account: {
|
||||
Args: { account_name: string; new_personal_code: string }
|
||||
Args:
|
||||
| { account_name: string }
|
||||
| { account_name: string; new_personal_code: string }
|
||||
Returns: {
|
||||
application_role: Database["medreport"]["Enums"]["application_role"]
|
||||
city: string | null
|
||||
@@ -1908,34 +1882,34 @@ export type Database = {
|
||||
get_account_invitations: {
|
||||
Args: { account_slug: string }
|
||||
Returns: {
|
||||
id: number
|
||||
email: string
|
||||
account_id: string
|
||||
invited_by: string
|
||||
role: string
|
||||
created_at: string
|
||||
updated_at: string
|
||||
email: string
|
||||
expires_at: string
|
||||
personal_code: string
|
||||
inviter_name: string
|
||||
id: number
|
||||
invited_by: string
|
||||
inviter_email: string
|
||||
inviter_name: string
|
||||
personal_code: string
|
||||
role: string
|
||||
updated_at: string
|
||||
}[]
|
||||
}
|
||||
get_account_members: {
|
||||
Args: { account_slug: string }
|
||||
Returns: {
|
||||
id: string
|
||||
user_id: string
|
||||
account_id: string
|
||||
role: string
|
||||
role_hierarchy_level: number
|
||||
primary_owner_user_id: string
|
||||
name: string
|
||||
created_at: string
|
||||
email: string
|
||||
id: string
|
||||
name: string
|
||||
personal_code: string
|
||||
picture_url: string
|
||||
created_at: string
|
||||
primary_owner_user_id: string
|
||||
role: string
|
||||
role_hierarchy_level: number
|
||||
updated_at: string
|
||||
user_id: string
|
||||
}[]
|
||||
}
|
||||
get_config: {
|
||||
@@ -1945,20 +1919,11 @@ export type Database = {
|
||||
get_invitations_with_account_ids: {
|
||||
Args: { company_id: string; personal_codes: string[] }
|
||||
Returns: {
|
||||
account_id: string
|
||||
invite_token: string
|
||||
personal_code: string
|
||||
account_id: string
|
||||
}[]
|
||||
}
|
||||
get_latest_medipost_dispatch_state_for_order: {
|
||||
Args: {
|
||||
medusa_order_id: string
|
||||
}
|
||||
Returns: {
|
||||
has_success: boolean
|
||||
action_date: string
|
||||
}
|
||||
}
|
||||
get_medipost_dispatch_tries: {
|
||||
Args: { p_medusa_order_id: string }
|
||||
Returns: number
|
||||
@@ -1985,17 +1950,17 @@ export type Database = {
|
||||
}
|
||||
has_more_elevated_role: {
|
||||
Args: {
|
||||
target_user_id: string
|
||||
target_account_id: string
|
||||
role_name: string
|
||||
target_account_id: string
|
||||
target_user_id: string
|
||||
}
|
||||
Returns: boolean
|
||||
}
|
||||
has_permission: {
|
||||
Args: {
|
||||
user_id: string
|
||||
account_id: string
|
||||
permission_name: Database["medreport"]["Enums"]["app_permissions"]
|
||||
user_id: string
|
||||
}
|
||||
Returns: boolean
|
||||
}
|
||||
@@ -2005,9 +1970,9 @@ export type Database = {
|
||||
}
|
||||
has_same_role_hierarchy_level: {
|
||||
Args: {
|
||||
target_user_id: string
|
||||
target_account_id: string
|
||||
role_name: string
|
||||
target_account_id: string
|
||||
target_user_id: string
|
||||
}
|
||||
Returns: boolean
|
||||
}
|
||||
@@ -2062,39 +2027,39 @@ export type Database = {
|
||||
team_account_workspace: {
|
||||
Args: { account_slug: string }
|
||||
Returns: {
|
||||
id: string
|
||||
name: string
|
||||
picture_url: string
|
||||
slug: string
|
||||
role: string
|
||||
role_hierarchy_level: number
|
||||
primary_owner_user_id: string
|
||||
subscription_status: Database["medreport"]["Enums"]["subscription_status"]
|
||||
permissions: Database["medreport"]["Enums"]["app_permissions"][]
|
||||
account_role: string
|
||||
application_role: Database["medreport"]["Enums"]["application_role"]
|
||||
id: string
|
||||
name: string
|
||||
permissions: Database["medreport"]["Enums"]["app_permissions"][]
|
||||
picture_url: string
|
||||
primary_owner_user_id: string
|
||||
role: string
|
||||
role_hierarchy_level: number
|
||||
slug: string
|
||||
subscription_status: Database["medreport"]["Enums"]["subscription_status"]
|
||||
}[]
|
||||
}
|
||||
transfer_team_account_ownership: {
|
||||
Args: { target_account_id: string; new_owner_id: string }
|
||||
Args: { new_owner_id: string; target_account_id: string }
|
||||
Returns: undefined
|
||||
}
|
||||
update_account: {
|
||||
Args: {
|
||||
p_name: string
|
||||
p_last_name: string
|
||||
p_personal_code: string
|
||||
p_phone: string
|
||||
p_city: string
|
||||
p_has_consent_personal_data: boolean
|
||||
p_last_name: string
|
||||
p_name: string
|
||||
p_personal_code: string
|
||||
p_phone: string
|
||||
p_uid: string
|
||||
}
|
||||
Returns: undefined
|
||||
}
|
||||
update_analysis_order_status: {
|
||||
Args: {
|
||||
order_id: number
|
||||
medusa_order_id_param: string
|
||||
order_id: number
|
||||
status_param: Database["medreport"]["Enums"]["analysis_order_status"]
|
||||
}
|
||||
Returns: {
|
||||
@@ -2109,14 +2074,14 @@ export type Database = {
|
||||
}
|
||||
upsert_order: {
|
||||
Args: {
|
||||
billing_provider: Database["medreport"]["Enums"]["billing_provider"]
|
||||
currency: string
|
||||
line_items: Json
|
||||
status: Database["medreport"]["Enums"]["payment_status"]
|
||||
target_account_id: string
|
||||
target_customer_id: string
|
||||
target_order_id: string
|
||||
status: Database["medreport"]["Enums"]["payment_status"]
|
||||
billing_provider: Database["medreport"]["Enums"]["billing_provider"]
|
||||
total_amount: number
|
||||
currency: string
|
||||
line_items: Json
|
||||
}
|
||||
Returns: {
|
||||
account_id: string
|
||||
@@ -2132,19 +2097,19 @@ export type Database = {
|
||||
}
|
||||
upsert_subscription: {
|
||||
Args: {
|
||||
target_account_id: string
|
||||
target_customer_id: string
|
||||
target_subscription_id: 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
|
||||
period_ends_at: string
|
||||
period_starts_at: string
|
||||
status: Database["medreport"]["Enums"]["subscription_status"]
|
||||
target_account_id: string
|
||||
target_customer_id: string
|
||||
target_subscription_id: string
|
||||
trial_ends_at?: string
|
||||
trial_starts_at?: string
|
||||
}
|
||||
Returns: {
|
||||
account_id: string
|
||||
@@ -2165,31 +2130,16 @@ export type Database = {
|
||||
}
|
||||
verify_nonce: {
|
||||
Args: {
|
||||
p_token: string
|
||||
p_purpose: string
|
||||
p_user_id?: string
|
||||
p_required_scopes?: string[]
|
||||
p_max_verification_attempts?: number
|
||||
p_ip?: unknown
|
||||
p_max_verification_attempts?: number
|
||||
p_purpose: string
|
||||
p_required_scopes?: string[]
|
||||
p_token: string
|
||||
p_user_agent?: string
|
||||
p_user_id?: string
|
||||
}
|
||||
Returns: Json
|
||||
}
|
||||
sync_analysis_results: {
|
||||
}
|
||||
send_medipost_test_response_for_order: {
|
||||
Args: {
|
||||
medusa_order_id: string
|
||||
}
|
||||
}
|
||||
order_has_medipost_dispatch_error: {
|
||||
Args: {
|
||||
medusa_order_id: string
|
||||
}
|
||||
Returns: {
|
||||
success: boolean
|
||||
}
|
||||
}
|
||||
}
|
||||
Enums: {
|
||||
analysis_feedback_status: "STARTED" | "DRAFT" | "COMPLETED"
|
||||
@@ -7918,9 +7868,9 @@ export type Database = {
|
||||
Functions: {
|
||||
has_permission: {
|
||||
Args: {
|
||||
user_id: string
|
||||
account_id: string
|
||||
permission_name: Database["public"]["Enums"]["app_permissions"]
|
||||
user_id: string
|
||||
}
|
||||
Returns: boolean
|
||||
}
|
||||
@@ -7970,21 +7920,25 @@ export type Database = {
|
||||
}
|
||||
}
|
||||
|
||||
type DefaultSchema = Database[Extract<keyof Database, "public">]
|
||||
type DatabaseWithoutInternals = Omit<Database, "__InternalSupabase">
|
||||
|
||||
type DefaultSchema = DatabaseWithoutInternals[Extract<keyof Database, "public">]
|
||||
|
||||
export type Tables<
|
||||
DefaultSchemaTableNameOrOptions extends
|
||||
| keyof (DefaultSchema["Tables"] & DefaultSchema["Views"])
|
||||
| { schema: keyof Database },
|
||||
| { schema: keyof DatabaseWithoutInternals },
|
||||
TableName extends DefaultSchemaTableNameOrOptions extends {
|
||||
schema: keyof Database
|
||||
schema: keyof DatabaseWithoutInternals
|
||||
}
|
||||
? keyof (Database[DefaultSchemaTableNameOrOptions["schema"]]["Tables"] &
|
||||
Database[DefaultSchemaTableNameOrOptions["schema"]]["Views"])
|
||||
? keyof (DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions["schema"]]["Tables"] &
|
||||
DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions["schema"]]["Views"])
|
||||
: never = never,
|
||||
> = DefaultSchemaTableNameOrOptions extends { schema: keyof Database }
|
||||
? (Database[DefaultSchemaTableNameOrOptions["schema"]]["Tables"] &
|
||||
Database[DefaultSchemaTableNameOrOptions["schema"]]["Views"])[TableName] extends {
|
||||
> = DefaultSchemaTableNameOrOptions extends {
|
||||
schema: keyof DatabaseWithoutInternals
|
||||
}
|
||||
? (DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions["schema"]]["Tables"] &
|
||||
DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions["schema"]]["Views"])[TableName] extends {
|
||||
Row: infer R
|
||||
}
|
||||
? R
|
||||
@@ -8002,14 +7956,16 @@ export type Tables<
|
||||
export type TablesInsert<
|
||||
DefaultSchemaTableNameOrOptions extends
|
||||
| keyof DefaultSchema["Tables"]
|
||||
| { schema: keyof Database },
|
||||
| { schema: keyof DatabaseWithoutInternals },
|
||||
TableName extends DefaultSchemaTableNameOrOptions extends {
|
||||
schema: keyof Database
|
||||
schema: keyof DatabaseWithoutInternals
|
||||
}
|
||||
? keyof Database[DefaultSchemaTableNameOrOptions["schema"]]["Tables"]
|
||||
? keyof DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions["schema"]]["Tables"]
|
||||
: never = never,
|
||||
> = DefaultSchemaTableNameOrOptions extends { schema: keyof Database }
|
||||
? Database[DefaultSchemaTableNameOrOptions["schema"]]["Tables"][TableName] extends {
|
||||
> = DefaultSchemaTableNameOrOptions extends {
|
||||
schema: keyof DatabaseWithoutInternals
|
||||
}
|
||||
? DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions["schema"]]["Tables"][TableName] extends {
|
||||
Insert: infer I
|
||||
}
|
||||
? I
|
||||
@@ -8025,14 +7981,16 @@ export type TablesInsert<
|
||||
export type TablesUpdate<
|
||||
DefaultSchemaTableNameOrOptions extends
|
||||
| keyof DefaultSchema["Tables"]
|
||||
| { schema: keyof Database },
|
||||
| { schema: keyof DatabaseWithoutInternals },
|
||||
TableName extends DefaultSchemaTableNameOrOptions extends {
|
||||
schema: keyof Database
|
||||
schema: keyof DatabaseWithoutInternals
|
||||
}
|
||||
? keyof Database[DefaultSchemaTableNameOrOptions["schema"]]["Tables"]
|
||||
? keyof DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions["schema"]]["Tables"]
|
||||
: never = never,
|
||||
> = DefaultSchemaTableNameOrOptions extends { schema: keyof Database }
|
||||
? Database[DefaultSchemaTableNameOrOptions["schema"]]["Tables"][TableName] extends {
|
||||
> = DefaultSchemaTableNameOrOptions extends {
|
||||
schema: keyof DatabaseWithoutInternals
|
||||
}
|
||||
? DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions["schema"]]["Tables"][TableName] extends {
|
||||
Update: infer U
|
||||
}
|
||||
? U
|
||||
@@ -8048,14 +8006,16 @@ export type TablesUpdate<
|
||||
export type Enums<
|
||||
DefaultSchemaEnumNameOrOptions extends
|
||||
| keyof DefaultSchema["Enums"]
|
||||
| { schema: keyof Database },
|
||||
| { schema: keyof DatabaseWithoutInternals },
|
||||
EnumName extends DefaultSchemaEnumNameOrOptions extends {
|
||||
schema: keyof Database
|
||||
schema: keyof DatabaseWithoutInternals
|
||||
}
|
||||
? keyof Database[DefaultSchemaEnumNameOrOptions["schema"]]["Enums"]
|
||||
? keyof DatabaseWithoutInternals[DefaultSchemaEnumNameOrOptions["schema"]]["Enums"]
|
||||
: never = never,
|
||||
> = DefaultSchemaEnumNameOrOptions extends { schema: keyof Database }
|
||||
? Database[DefaultSchemaEnumNameOrOptions["schema"]]["Enums"][EnumName]
|
||||
> = DefaultSchemaEnumNameOrOptions extends {
|
||||
schema: keyof DatabaseWithoutInternals
|
||||
}
|
||||
? DatabaseWithoutInternals[DefaultSchemaEnumNameOrOptions["schema"]]["Enums"][EnumName]
|
||||
: DefaultSchemaEnumNameOrOptions extends keyof DefaultSchema["Enums"]
|
||||
? DefaultSchema["Enums"][DefaultSchemaEnumNameOrOptions]
|
||||
: never
|
||||
@@ -8063,14 +8023,16 @@ export type Enums<
|
||||
export type CompositeTypes<
|
||||
PublicCompositeTypeNameOrOptions extends
|
||||
| keyof DefaultSchema["CompositeTypes"]
|
||||
| { schema: keyof Database },
|
||||
| { schema: keyof DatabaseWithoutInternals },
|
||||
CompositeTypeName extends PublicCompositeTypeNameOrOptions extends {
|
||||
schema: keyof Database
|
||||
schema: keyof DatabaseWithoutInternals
|
||||
}
|
||||
? keyof Database[PublicCompositeTypeNameOrOptions["schema"]]["CompositeTypes"]
|
||||
? keyof DatabaseWithoutInternals[PublicCompositeTypeNameOrOptions["schema"]]["CompositeTypes"]
|
||||
: never = never,
|
||||
> = PublicCompositeTypeNameOrOptions extends { schema: keyof Database }
|
||||
? Database[PublicCompositeTypeNameOrOptions["schema"]]["CompositeTypes"][CompositeTypeName]
|
||||
> = PublicCompositeTypeNameOrOptions extends {
|
||||
schema: keyof DatabaseWithoutInternals
|
||||
}
|
||||
? DatabaseWithoutInternals[PublicCompositeTypeNameOrOptions["schema"]]["CompositeTypes"][CompositeTypeName]
|
||||
: PublicCompositeTypeNameOrOptions extends keyof DefaultSchema["CompositeTypes"]
|
||||
? DefaultSchema["CompositeTypes"][PublicCompositeTypeNameOrOptions]
|
||||
: never
|
||||
|
||||
@@ -13,7 +13,7 @@ const message =
|
||||
export function getServiceRoleKey() {
|
||||
return z
|
||||
.string({
|
||||
required_error: message,
|
||||
error: message,
|
||||
})
|
||||
.min(1, {
|
||||
message: message,
|
||||
|
||||
@@ -7,14 +7,14 @@ export function getSupabaseClientKeys() {
|
||||
return z
|
||||
.object({
|
||||
url: z.string({
|
||||
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`,
|
||||
}),
|
||||
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
|
||||
.string({
|
||||
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`,
|
||||
error: `Please provide the variable NEXT_PUBLIC_SUPABASE_ANON_KEY`,
|
||||
})
|
||||
.describe(`This is the anon key provided by Supabase. It is a public key used client-side. Please provide the variable NEXT_PUBLIC_SUPABASE_ANON_KEY.`)
|
||||
.min(1),
|
||||
})
|
||||
.parse({
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
|
||||
import { useSupabase } from './use-supabase';
|
||||
import { signout } from '../../../features/medusa-storefront/src/lib/data/customer';
|
||||
|
||||
export function useSignOut() {
|
||||
const client = useSupabase();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: () => {
|
||||
mutationFn: async () => {
|
||||
await signout(undefined, false);
|
||||
return client.auth.signOut();
|
||||
},
|
||||
});
|
||||
|
||||
@@ -14,7 +14,7 @@ export function useUser(initialData?: User | null) {
|
||||
|
||||
// this is most likely a session error or the user is not logged in
|
||||
if (response.error) {
|
||||
return undefined;
|
||||
return null;
|
||||
}
|
||||
|
||||
if (response.data?.user) {
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
const RouteMatchingEnd = z
|
||||
.union([z.boolean(), z.function().args(z.string()).returns(z.boolean())])
|
||||
.union([
|
||||
z.boolean(),
|
||||
z.function({ input: [z.string()], output: z.boolean() }),
|
||||
])
|
||||
.default(false)
|
||||
.optional();
|
||||
|
||||
|
||||
3809
pnpm-lock.yaml
generated
3809
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1,133 +1,169 @@
|
||||
{
|
||||
"accountTabLabel": "Account Settings",
|
||||
"accountTabDescription": "Manage your account settings",
|
||||
"accountTabLabel": "Account settings",
|
||||
"accountTabDescription": "Manage your account settings and email preferences.",
|
||||
"preferencesTabLabel": "Preferences",
|
||||
"preferencesTabDescription": "Manage your preferences.",
|
||||
"securityTabLabel": "Security",
|
||||
"securityTabDescription": "Protect your account.",
|
||||
"homePage": "Home",
|
||||
"billingTab": "Billing",
|
||||
"settingsTab": "Settings",
|
||||
"multiFactorAuth": "Multi-Factor Authentication",
|
||||
"multiFactorAuthDescription": "Set up Multi-Factor Authentication method to further secure your account",
|
||||
"multiFactorAuth": "Multi-factor authentication",
|
||||
"multiFactorAuthDescription": "Set up multi-factor authentication to better protect your account",
|
||||
"updateProfileSuccess": "Profile successfully updated",
|
||||
"updateProfileError": "Encountered an error. Please try again",
|
||||
"updatePasswordSuccess": "Password update request successful",
|
||||
"updateProfileError": "An error occurred. Please try again",
|
||||
"updatePasswordSuccess": "Password update successful",
|
||||
"updatePasswordSuccessMessage": "Your password has been successfully updated!",
|
||||
"updatePasswordError": "Encountered an error. Please try again",
|
||||
"updatePasswordError": "An error occurred. Please try again",
|
||||
"updatePasswordLoading": "Updating password...",
|
||||
"updateProfileLoading": "Updating profile...",
|
||||
"name": "Your Name",
|
||||
"nameDescription": "Update your name to be displayed on your profile",
|
||||
"emailLabel": "Email Address",
|
||||
"accountImage": "Your Profile Picture",
|
||||
"accountImageDescription": "Please choose a photo to upload as your profile picture.",
|
||||
"profilePictureHeading": "Upload a Profile Picture",
|
||||
"name": "Your name",
|
||||
"nameDescription": "Update the name displayed on your profile",
|
||||
"emailLabel": "Email address",
|
||||
"accountImage": "Your profile picture",
|
||||
"accountImageDescription": "Choose a photo to upload as your profile picture.",
|
||||
"profilePictureHeading": "Upload a profile picture",
|
||||
"profilePictureSubheading": "Choose a photo to upload as your profile picture.",
|
||||
"updateProfileSubmitLabel": "Update Profile",
|
||||
"updatePasswordCardTitle": "Update your Password",
|
||||
"updateProfileSubmitLabel": "Update profile",
|
||||
"updatePasswordCardTitle": "Update your password",
|
||||
"updatePasswordCardDescription": "Update your password to keep your account secure.",
|
||||
"currentPassword": "Current Password",
|
||||
"newPassword": "New Password",
|
||||
"repeatPassword": "Repeat New Password",
|
||||
"currentPassword": "Current password",
|
||||
"newPassword": "New password",
|
||||
"repeatPassword": "Repeat new password",
|
||||
"repeatPasswordDescription": "Please repeat your new password to confirm it",
|
||||
"yourPassword": "Your Password",
|
||||
"updatePasswordSubmitLabel": "Update Password",
|
||||
"updateEmailCardTitle": "Update your Email",
|
||||
"updateEmailCardDescription": "Update your email address you use to login to your account",
|
||||
"newEmail": "Your New Email",
|
||||
"repeatEmail": "Repeat Email",
|
||||
"updateEmailSubmitLabel": "Update Email Address",
|
||||
"updateEmailSuccess": "Email update request successful",
|
||||
"updateEmailSuccessMessage": "We sent you an email to confirm your new email address. Please check your inbox and click on the link to confirm your new email address.",
|
||||
"updateEmailLoading": "Updating your email...",
|
||||
"yourPassword": "Your password",
|
||||
"updatePasswordSubmitLabel": "Update password",
|
||||
"updateEmailCardTitle": "Update your email",
|
||||
"updateEmailCardDescription": "Update the email address you use to log in",
|
||||
"newEmail": "Your new email",
|
||||
"repeatEmail": "Repeat email",
|
||||
"updateEmailSubmitLabel": "Update email address",
|
||||
"updateEmailSuccess": "Email update successful",
|
||||
"updateEmailSuccessMessage": "We will send you a confirmation email to verify your new address. Please check your inbox and click the link.",
|
||||
"updateEmailLoading": "Updating email...",
|
||||
"updateEmailError": "Email not updated. Please try again",
|
||||
"passwordNotMatching": "Passwords do not match. Make sure you're using the correct password",
|
||||
"emailNotMatching": "Emails do not match. Make sure you're using the correct email",
|
||||
"passwordNotChanged": "Your password has not changed",
|
||||
"emailsNotMatching": "Emails do not match. Make sure you're using the correct email",
|
||||
"cannotUpdatePassword": "You cannot update your password because your account is not linked to any.",
|
||||
"setupMfaButtonLabel": "Setup a new Factor",
|
||||
"multiFactorSetupErrorHeading": "Setup Failed",
|
||||
"multiFactorSetupErrorDescription": "Sorry, there was an error while setting up your factor. Please try again.",
|
||||
"multiFactorAuthHeading": "Secure your account with Multi-Factor Authentication",
|
||||
"multiFactorModalHeading": "Use your authenticator app to scan the QR code below. Then enter the code generated.",
|
||||
"factorNameLabel": "A memorable name to identify this factor",
|
||||
"factorNameHint": "Use an easy-to-remember name to easily identify this factor in the future. Ex. iPhone 14",
|
||||
"passwordNotMatching": "Passwords do not match. Make sure you are using the correct password",
|
||||
"emailNotMatching": "Emails do not match. Make sure you are using the correct email",
|
||||
"passwordNotChanged": "Your password has not been changed",
|
||||
"emailsNotMatching": "Emails do not match. Make sure you are using the correct email",
|
||||
"cannotUpdatePassword": "You cannot update your password because your account is not linked to a password.",
|
||||
"setupMfaButtonLabel": "Set up new factor",
|
||||
"multiFactorSetupErrorHeading": "Setup failed",
|
||||
"multiFactorSetupErrorDescription": "Sorry, an error occurred while setting up the factor. Please try again.",
|
||||
"multiFactorAuthHeading": "Protect your account with multi-factor authentication",
|
||||
"multiFactorModalHeading": "Use your authentication app to scan the QR code. Then enter the generated code.",
|
||||
"factorNameLabel": "Memorable name for factor identification",
|
||||
"factorNameHint": "Use a simple name to easily identify this factor later. E.g. iPhone 14",
|
||||
"factorNameSubmitLabel": "Set factor name",
|
||||
"unenrollTooltip": "Unenroll this factor",
|
||||
"unenrollingFactor": "Unenrolling factor...",
|
||||
"unenrollFactorSuccess": "Factor successfully unenrolled",
|
||||
"unenrollFactorError": "Unenrolling factor failed",
|
||||
"unenrollTooltip": "Unregister this factor",
|
||||
"unenrollingFactor": "Unregistering factor...",
|
||||
"unenrollFactorSuccess": "Factor successfully removed",
|
||||
"unenrollFactorError": "Failed to remove factor",
|
||||
"factorsListError": "Error loading factors list",
|
||||
"factorsListErrorDescription": "Sorry, we couldn't load the factors list. Please try again.",
|
||||
"factorName": "Factor Name",
|
||||
"factorsListErrorDescription": "Sorry, we could not load the factors list. Please try again.",
|
||||
"factorName": "Factor name",
|
||||
"factorType": "Type",
|
||||
"factorStatus": "Status",
|
||||
"mfaEnabledSuccessTitle": "Multi-Factor authentication is enabled",
|
||||
"mfaEnabledSuccessDescription": "Congratulations! You have successfully enrolled in the multi factor authentication process. You will now be able to access your account with a combination of your password and an authentication code sent to your phone number.",
|
||||
"verificationCode": "Verification Code",
|
||||
"addEmailAddress": "Add Email address",
|
||||
"verifyActivationCodeDescription": "Enter the 6-digit code generated by your authenticator app in the field above",
|
||||
"mfaEnabledSuccessTitle": "Multi-factor authentication enabled",
|
||||
"mfaEnabledSuccessDescription": "Congratulations! You have successfully registered for multi-factor authentication. You can now log in with your password and authentication code.",
|
||||
"verificationCode": "Verification code",
|
||||
"addEmailAddress": "Add email address",
|
||||
"verifyActivationCodeDescription": "Enter the 6-digit code generated by your authentication app",
|
||||
"loadingFactors": "Loading factors...",
|
||||
"enableMfaFactor": "Enable Factor",
|
||||
"disableMfaFactor": "Disable Factor",
|
||||
"qrCodeErrorHeading": "QR Code Error",
|
||||
"qrCodeErrorDescription": "Sorry, we weren't able to generate the QR code",
|
||||
"multiFactorSetupSuccess": "Factor successfully enrolled",
|
||||
"submitVerificationCode": "Submit Verification Code",
|
||||
"mfaEnabledSuccessAlert": "Multi-Factor authentication is enabled",
|
||||
"enableMfaFactor": "Enable factor",
|
||||
"disableMfaFactor": "Disable factor",
|
||||
"qrCodeErrorHeading": "QR code error",
|
||||
"qrCodeErrorDescription": "Sorry, QR code generation failed",
|
||||
"multiFactorSetupSuccess": "Factor successfully registered",
|
||||
"submitVerificationCode": "Submit verification code",
|
||||
"mfaEnabledSuccessAlert": "Multi-factor authentication enabled",
|
||||
"verifyingCode": "Verifying code...",
|
||||
"invalidVerificationCodeHeading": "Invalid Verification Code",
|
||||
"invalidVerificationCodeDescription": "The verification code you entered is invalid. Please try again.",
|
||||
"unenrollFactorModalHeading": "Unenroll Factor",
|
||||
"unenrollFactorModalDescription": "You're about to unenroll this factor. You will not be able to use it to login to your account.",
|
||||
"unenrollFactorModalBody": "You're about to unenroll this factor. You will not be able to use it to login to your account.",
|
||||
"unenrollFactorModalButtonLabel": "Yes, unenroll factor",
|
||||
"selectFactor": "Choose a factor to verify your identity",
|
||||
"disableMfa": "Disable Multi-Factor Authentication",
|
||||
"invalidVerificationCodeHeading": "Invalid verification code",
|
||||
"invalidVerificationCodeDescription": "The entered verification code is not valid. Please try again.",
|
||||
"unenrollFactorModalHeading": "Unregister factor",
|
||||
"unenrollFactorModalDescription": "You are about to unregister this factor. You will no longer be able to use it for login.",
|
||||
"unenrollFactorModalBody": "You are about to unregister this factor. You will no longer be able to use it for login.",
|
||||
"unenrollFactorModalButtonLabel": "Yes, remove factor",
|
||||
"selectFactor": "Select a factor to verify your identity",
|
||||
"disableMfa": "Disable multi-factor authentication",
|
||||
"disableMfaButtonLabel": "Disable MFA",
|
||||
"confirmDisableMfaButtonLabel": "Yes, disable MFA",
|
||||
"disablingMfa": "Disabling Multi-Factor Authentication. Please wait...",
|
||||
"disableMfaSuccess": "Multi-Factor Authentication successfully disabled",
|
||||
"disableMfaError": "Sorry, we encountered an error. MFA has not been disabled.",
|
||||
"sendingEmailVerificationLink": "Sending Email...",
|
||||
"sendEmailVerificationLinkSuccess": "Verification link successfully sent",
|
||||
"sendEmailVerificationLinkError": "Sorry, we weren't able to send you the email",
|
||||
"sendVerificationLinkSubmitLabel": "Send Verification Link",
|
||||
"sendVerificationLinkSuccessLabel": "Email sent! Check your Inbox",
|
||||
"disablingMfa": "Disabling multi-factor authentication. Please wait...",
|
||||
"disableMfaSuccess": "Multi-factor authentication successfully disabled",
|
||||
"disableMfaError": "Sorry, an error occurred. MFA was not disabled.",
|
||||
"sendingEmailVerificationLink": "Sending email...",
|
||||
"sendEmailVerificationLinkSuccess": "Confirmation link successfully sent",
|
||||
"sendEmailVerificationLinkError": "Sorry, sending email failed",
|
||||
"sendVerificationLinkSubmitLabel": "Send confirmation link",
|
||||
"sendVerificationLinkSuccessLabel": "Email sent! Check your inbox",
|
||||
"verifyEmailAlertHeading": "Please verify your email to enable MFA",
|
||||
"verificationLinkAlertDescription": "Your email is not yet verified. Please verify your email to be able to set up Multi-Factor Authentication.",
|
||||
"authFactorName": "Factor Name (optional)",
|
||||
"authFactorNameHint": "Assign a name that helps you remember the phone number used",
|
||||
"loadingUser": "Loading user details. Please wait...",
|
||||
"linkPhoneNumber": "Link Phone Number",
|
||||
"dangerZone": "Danger Zone",
|
||||
"dangerZoneDescription": "Some actions cannot be undone. Please be careful.",
|
||||
"deleteAccount": "Delete your Account",
|
||||
"verificationLinkAlertDescription": "Your email has not yet been verified. Please confirm your email to set up multi-factor authentication.",
|
||||
"authFactorName": "Factor name (optional)",
|
||||
"authFactorNameHint": "Set a name to help remember the phone number used",
|
||||
"loadingUser": "Loading user data. Please wait...",
|
||||
"linkPhoneNumber": "Link phone number",
|
||||
"dangerZone": "Danger zone",
|
||||
"dangerZoneDescription": "Some actions cannot be undone. Be careful.",
|
||||
"deleteAccount": "Delete your account",
|
||||
"deletingAccount": "Deleting account. Please wait...",
|
||||
"deleteAccountDescription": "This will delete your account and the accounts you own. Furthermore, we will immediately cancel any active subscriptions. This action cannot be undone.",
|
||||
"deleteAccountDescription": "This will delete your account and all accounts you own. All active subscriptions will also be immediately canceled. This action cannot be undone.",
|
||||
"deleteProfileConfirmationInputLabel": "Type DELETE to confirm",
|
||||
"deleteAccountErrorHeading": "Sorry, we couldn't delete your account",
|
||||
"needsReauthentication": "Reauthentication Required",
|
||||
"needsReauthenticationDescription": "You need to reauthenticate to change your password. Please sign out and sign in again to change your password.",
|
||||
"deleteAccountErrorHeading": "Sorry, we could not delete your account",
|
||||
"needsReauthentication": "Re-authentication required",
|
||||
"needsReauthenticationDescription": "You must re-authenticate to change your password. Please log out and back in to change it.",
|
||||
"language": "Language",
|
||||
"languageDescription": "Choose your preferred language",
|
||||
"noTeamsYet": "You don't have any teams yet.",
|
||||
"noTeamsYet": "You don’t have any teams yet.",
|
||||
"createTeam": "Create a team to get started.",
|
||||
"createTeamButtonLabel": "Create a Team",
|
||||
"createCompanyAccount": "Create Company Account",
|
||||
"createTeamButtonLabel": "Create team",
|
||||
"createCompanyAccount": "Create a company account",
|
||||
"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"
|
||||
},
|
||||
"updateConsentSuccess": "Consent successfully updated",
|
||||
"updateConsentError": "Encountered an error. Please try again",
|
||||
"updateConsentLoading": "Updating consent...",
|
||||
"updateAccount": {
|
||||
"title": "Personal details",
|
||||
"description": "Please enter your personal details to continue",
|
||||
"button": "Continue",
|
||||
"userConsentLabel": "I agree to the use of personal data on the platform",
|
||||
"userConsentUrlTitle": "View privacy policy"
|
||||
},
|
||||
"consentModal": {
|
||||
"title": "Before we start",
|
||||
"description": "Do you consent to your health data being used anonymously in employer statistics? The data remains anonymized and helps companies better support employee health.",
|
||||
"reject": "Do not consent",
|
||||
"accept": "Consent"
|
||||
},
|
||||
"updateConsentSuccess": "Consents updated",
|
||||
"updateConsentError": "Something went wrong. Please try again",
|
||||
"updateConsentLoading": "Updating consents...",
|
||||
"consentToAnonymizedCompanyData": {
|
||||
"label": "Consent to be included in employer statistics",
|
||||
"description": "Consent to be included in anonymized company statistics"
|
||||
"label": "I agree to participate in employer statistics",
|
||||
"description": "I agree to the use of anonymized health data in employer statistics"
|
||||
},
|
||||
"membershipConfirmation": {
|
||||
"successTitle": "Hello, {{firstName}} {{lastName}}",
|
||||
"successDescription": "Your health account has been activated and is ready to use!",
|
||||
"successButton": "Continue"
|
||||
},
|
||||
"updateRoleSuccess": "Role updated",
|
||||
"updateRoleError": "Something went wrong, please try again",
|
||||
"updateRoleError": "Something went wrong. Please try again",
|
||||
"updateRoleLoading": "Updating role...",
|
||||
"updatePreferredLocaleSuccess": "Language preference updated",
|
||||
"updatePreferredLocaleError": "Language preference update failed",
|
||||
"updatePreferredLocaleLoading": "Updating language preference...",
|
||||
"doctorAnalysisSummary": "Doctor's summary"
|
||||
"updatePreferredLocaleSuccess": "Preferred language updated",
|
||||
"updatePreferredLocaleError": "Failed to update preferred language",
|
||||
"updatePreferredLocaleLoading": "Updating preferred language...",
|
||||
"doctorAnalysisSummary": "Doctor’s summary of test results",
|
||||
"myHabits": "My health habits",
|
||||
"formField": {
|
||||
"smoking": "I smoke"
|
||||
},
|
||||
"updateAccountSuccess": "Account details updated",
|
||||
"updateAccountError": "Updating account details failed",
|
||||
"updateAccountPreferencesSuccess": "Account preferences updated",
|
||||
"updateAccountPreferencesError": "Updating account preferences failed",
|
||||
"consents": "Consents"
|
||||
}
|
||||
@@ -5,6 +5,7 @@
|
||||
"emptyCartMessageDescription": "Add items to your cart to continue.",
|
||||
"subtotal": "Subtotal",
|
||||
"total": "Total",
|
||||
"promotionsTotal": "Promotions total",
|
||||
"table": {
|
||||
"item": "Item",
|
||||
"quantity": "Quantity",
|
||||
@@ -24,10 +25,13 @@
|
||||
"timeoutAction": "Continue"
|
||||
},
|
||||
"discountCode": {
|
||||
"title": "Gift card or promotion code",
|
||||
"label": "Add Promotion Code(s)",
|
||||
"apply": "Apply",
|
||||
"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": {
|
||||
"synlabAnalyses": {
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
{
|
||||
"homeTabLabel": "Home",
|
||||
"homeTabDescription": "Welcome to your home page",
|
||||
"homeTabDescription": "Welcome to your homepage",
|
||||
"accountMembers": "Company Members",
|
||||
"membersTabDescription": "Here you can manage the members of your company.",
|
||||
"membersTabDescription": "Here you can manage your company members.",
|
||||
"billingTabLabel": "Billing",
|
||||
"billingTabDescription": "Manage your billing and subscription",
|
||||
"billingTabDescription": "Manage your billing and subscriptions",
|
||||
"dashboardTabLabel": "Dashboard",
|
||||
"settingsTabLabel": "Settings",
|
||||
"profileSettingsTabLabel": "Profile",
|
||||
"subscriptionSettingsTabLabel": "Subscription",
|
||||
"dashboardTabDescription": "An overview of your account's activity and performance across all your projects.",
|
||||
"dashboardTabDescription": "Overview of your account activity and project results.",
|
||||
"settingsTabDescription": "Manage your settings and preferences.",
|
||||
"emailAddress": "Email Address",
|
||||
"password": "Password",
|
||||
@@ -18,69 +18,72 @@
|
||||
"cancel": "Cancel",
|
||||
"clear": "Clear",
|
||||
"close": "Close",
|
||||
"notFound": "Not Found",
|
||||
"backToHomePage": "Back to Home Page",
|
||||
"goBack": "Go Back",
|
||||
"notFound": "Not found",
|
||||
"backToHomePage": "Back to homepage",
|
||||
"goBack": "Go back",
|
||||
"genericServerError": "Sorry, something went wrong.",
|
||||
"genericServerErrorHeading": "Sorry, something went wrong while processing your request. Please contact us if the issue persists.",
|
||||
"pageNotFound": "Sorry, this page does not exist.",
|
||||
"pageNotFoundSubHeading": "Apologies, the page you were looking for was not found",
|
||||
"pageNotFoundSubHeading": "Sorry, the page you were looking for was not found",
|
||||
"genericError": "Sorry, something went wrong.",
|
||||
"genericErrorSubHeading": "Apologies, an error occurred while processing your request. Please contact us if the issue persists.",
|
||||
"genericErrorSubHeading": "An error occurred while processing your request. Please contact us if the issue persists.",
|
||||
"anonymousUser": "Anonymous",
|
||||
"tryAgain": "Try Again",
|
||||
"tryAgain": "Try again",
|
||||
"theme": "Theme",
|
||||
"lightTheme": "Light",
|
||||
"darkTheme": "Dark",
|
||||
"systemTheme": "System",
|
||||
"expandSidebar": "Expand Sidebar",
|
||||
"collapseSidebar": "Collapse Sidebar",
|
||||
"expandSidebar": "Expand sidebar",
|
||||
"collapseSidebar": "Collapse sidebar",
|
||||
"documentation": "Documentation",
|
||||
"getStarted": "Get Started",
|
||||
"getStartedWithPlan": "Get Started with {{plan}}",
|
||||
"getStarted": "Get started!",
|
||||
"getStartedWithPlan": "Get started with plan {{plan}}",
|
||||
"retry": "Retry",
|
||||
"contactUs": "Contact Us",
|
||||
"contactUs": "Contact us",
|
||||
"loading": "Loading. Please wait...",
|
||||
"yourAccounts": "Your Accounts",
|
||||
"yourAccounts": "Your accounts",
|
||||
"continue": "Continue",
|
||||
"skip": "Skip",
|
||||
"signedInAs": "Signed in as",
|
||||
"pageOfPages": "Page {{page}} of {{total}}",
|
||||
"noData": "No data available",
|
||||
"pageNotFoundHeading": "Ouch! :|",
|
||||
"errorPageHeading": "Ouch! :|",
|
||||
"pageOfPages": "Page {{page}} / {{total}}",
|
||||
"noData": "No data",
|
||||
"pageNotFoundHeading": "Oops! :|",
|
||||
"errorPageHeading": "Oops! :|",
|
||||
"notifications": "Notifications",
|
||||
"noNotifications": "No notifications",
|
||||
"justNow": "Just now",
|
||||
"newVersionAvailable": "New version available",
|
||||
"newVersionAvailableDescription": "A new version of the app is available. It is recommended to refresh the page to get the latest updates and avoid any issues.",
|
||||
"newVersionSubmitButton": "Reload and Update",
|
||||
"newVersionAvailableDescription": "A new version of the app is available. We recommend refreshing the page to get the latest updates and avoid issues.",
|
||||
"newVersionSubmitButton": "Refresh and update",
|
||||
"back": "Back",
|
||||
"welcome": "Welcome",
|
||||
"shoppingCart": "Shopping cart",
|
||||
"shoppingCartCount": "Shopping cart ({{count}})",
|
||||
"shoppingCart": "Shopping Cart",
|
||||
"shoppingCartCount": "Shopping Cart ({{count}})",
|
||||
"search": "Search{{end}}",
|
||||
"myActions": "My actions",
|
||||
"healthPackageComparison": {
|
||||
"label": "Health package comparison",
|
||||
"description": "Alljärgnevalt on antud eelinfo (sugu, vanus ja kehamassiindeksi) põhjal tehtud personalne terviseauditi valik. Tabelis on võimalik soovitatud terviseuuringute paketile lisada üksikuid uuringuid juurde."
|
||||
"label": "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."
|
||||
},
|
||||
"routes": {
|
||||
"home": "Home",
|
||||
"overview": "Overview",
|
||||
"booking": "Booking",
|
||||
"booking": "Book appointment",
|
||||
"myOrders": "My orders",
|
||||
"analysisResults": "Analysis results",
|
||||
"orderAnalysisPackage": "Telli analüüside pakett",
|
||||
"orderAnalysisPackage": "Order analysis package",
|
||||
"orderAnalysis": "Order analysis",
|
||||
"orderHealthAnalysis": "Telli terviseuuring",
|
||||
"orderHealthAnalysis": "Order health check",
|
||||
"account": "Account",
|
||||
"members": "Members",
|
||||
"billing": "Billing",
|
||||
"dashboard": "Dashboard",
|
||||
"settings": "Settings",
|
||||
"profile": "Profile",
|
||||
"application": "Application"
|
||||
"application": "Application",
|
||||
"pickTime": "Pick time",
|
||||
"preferences": "Preferences",
|
||||
"security": "Security"
|
||||
},
|
||||
"roles": {
|
||||
"owner": {
|
||||
@@ -91,31 +94,53 @@
|
||||
}
|
||||
},
|
||||
"otp": {
|
||||
"requestVerificationCode": "Request Verification Code",
|
||||
"requestVerificationCodeDescription": "We must verify your identity to continue with this action. We'll send a verification code to the email address {{email}}.",
|
||||
"sendingCode": "Sending Code...",
|
||||
"sendVerificationCode": "Send Verification Code",
|
||||
"enterVerificationCode": "Enter Verification Code",
|
||||
"codeSentToEmail": "We've sent a verification code to the email address {{email}}.",
|
||||
"verificationCode": "Verification Code",
|
||||
"enterCodeFromEmail": "Enter the 6-digit code we sent to your email.",
|
||||
"requestVerificationCode": "Please request a verification code",
|
||||
"requestVerificationCodeDescription": "We need to verify your identity before continuing. We will send a code to your email address {{email}}.",
|
||||
"sendingCode": "Sending code...",
|
||||
"sendVerificationCode": "Send verification code",
|
||||
"enterVerificationCode": "Enter verification code",
|
||||
"codeSentToEmail": "We have sent a code to your email address {{email}}.",
|
||||
"verificationCode": "Verification code",
|
||||
"enterCodeFromEmail": "Enter the 6-digit code we sent to your email address.",
|
||||
"verifying": "Verifying...",
|
||||
"verifyCode": "Verify Code",
|
||||
"requestNewCode": "Request New Code",
|
||||
"verifyCode": "Verify code",
|
||||
"requestNewCode": "Request new code",
|
||||
"errorSendingCode": "Error sending code. Please try again."
|
||||
},
|
||||
"cookieBanner": {
|
||||
"title": "Hey, we use cookies 🍪",
|
||||
"description": "This website uses cookies to ensure you get the best experience on our website.",
|
||||
"description": "This website uses cookies to ensure the best experience.",
|
||||
"reject": "Reject",
|
||||
"accept": "Accept"
|
||||
},
|
||||
"formField": {
|
||||
"companyName": "Company name",
|
||||
"contactPerson": "Contact person",
|
||||
"email": "Email",
|
||||
"phone": "Phone",
|
||||
"firstName": "First name",
|
||||
"lastName": "Last name",
|
||||
"personalCode": "Personal code",
|
||||
"city": "City",
|
||||
"weight": "Weight",
|
||||
"height": "Height",
|
||||
"occurance": "Support frequency",
|
||||
"amount": "Amount",
|
||||
"selectDate": "Select date"
|
||||
},
|
||||
"wallet": {
|
||||
"balance": "Your MedReport account balance",
|
||||
"expiredAt": "Valid until {{expiredAt}}"
|
||||
},
|
||||
"doctor": "Doctor",
|
||||
"save": "Save",
|
||||
"saveAsDraft": "Save as draft",
|
||||
"confirm": "Confirm",
|
||||
"previous": "Previous",
|
||||
"next": "Next",
|
||||
"invalidDataError": "Invalid data submitted",
|
||||
"language": "Language"
|
||||
"invalidDataError": "Invalid data",
|
||||
"language": "Language",
|
||||
"yes": "Yes",
|
||||
"no": "No",
|
||||
"preferNotToAnswer": "Prefer not to answer"
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"recentlyCheckedDescription": "Super, oled käinud tervist kontrollimas. Siin on sinule olulised näitajad",
|
||||
"recentlyCheckedDescription": "Here are your most important health indicators.",
|
||||
"respondToQuestion": "Respond",
|
||||
"gender": "Gender",
|
||||
"male": "Male",
|
||||
|
||||
7
public/locales/en/error.json
Normal file
7
public/locales/en/error.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"invalidNumber": "Invalid number",
|
||||
"invalidEmail": "Invalid email",
|
||||
"tooShort": "Too short",
|
||||
"tooLong": "Too long",
|
||||
"invalidPhone": "Invalid phone"
|
||||
}
|
||||
@@ -1,125 +1,129 @@
|
||||
{
|
||||
"accountTabLabel": "Account Settings",
|
||||
"accountTabDescription": "Manage your account settings",
|
||||
"homePage": "Home",
|
||||
"billingTab": "Billing",
|
||||
"settingsTab": "Settings",
|
||||
"multiFactorAuth": "Multi-Factor Authentication",
|
||||
"multiFactorAuthDescription": "Set up Multi-Factor Authentication method to further secure your account",
|
||||
"updateProfileSuccess": "Profile successfully updated",
|
||||
"updateProfileError": "Encountered an error. Please try again",
|
||||
"updatePasswordSuccess": "Password update request successful",
|
||||
"updatePasswordSuccessMessage": "Your password has been successfully updated!",
|
||||
"updatePasswordError": "Encountered an error. Please try again",
|
||||
"updatePasswordLoading": "Updating password...",
|
||||
"updateProfileLoading": "Updating profile...",
|
||||
"name": "Your Name",
|
||||
"nameDescription": "Update your name to be displayed on your profile",
|
||||
"emailLabel": "Email Address",
|
||||
"accountImage": "Your Profile Picture",
|
||||
"accountImageDescription": "Please choose a photo to upload as your profile picture.",
|
||||
"profilePictureHeading": "Upload a Profile Picture",
|
||||
"profilePictureSubheading": "Choose a photo to upload as your profile picture.",
|
||||
"updateProfileSubmitLabel": "Update Profile",
|
||||
"updatePasswordCardTitle": "Update your Password",
|
||||
"updatePasswordCardDescription": "Update your password to keep your account secure.",
|
||||
"currentPassword": "Current Password",
|
||||
"newPassword": "New Password",
|
||||
"repeatPassword": "Repeat New Password",
|
||||
"repeatPasswordDescription": "Please repeat your new password to confirm it",
|
||||
"yourPassword": "Your Password",
|
||||
"updatePasswordSubmitLabel": "Update Password",
|
||||
"updateEmailCardTitle": "Update your Email",
|
||||
"updateEmailCardDescription": "Update your email address you use to login to your account",
|
||||
"newEmail": "Your New Email",
|
||||
"repeatEmail": "Repeat Email",
|
||||
"updateEmailSubmitLabel": "Update Email Address",
|
||||
"updateEmailSuccess": "Email update request successful",
|
||||
"updateEmailSuccessMessage": "We sent you an email to confirm your new email address. Please check your inbox and click on the link to confirm your new email address.",
|
||||
"updateEmailLoading": "Updating your email...",
|
||||
"updateEmailError": "Email not updated. Please try again",
|
||||
"passwordNotMatching": "Passwords do not match. Make sure you're using the correct password",
|
||||
"emailNotMatching": "Emails do not match. Make sure you're using the correct email",
|
||||
"passwordNotChanged": "Your password has not changed",
|
||||
"emailsNotMatching": "Emails do not match. Make sure you're using the correct email",
|
||||
"cannotUpdatePassword": "You cannot update your password because your account is not linked to any.",
|
||||
"setupMfaButtonLabel": "Setup a new Factor",
|
||||
"multiFactorSetupErrorHeading": "Setup Failed",
|
||||
"multiFactorSetupErrorDescription": "Sorry, there was an error while setting up your factor. Please try again.",
|
||||
"multiFactorAuthHeading": "Secure your account with Multi-Factor Authentication",
|
||||
"multiFactorModalHeading": "Use your authenticator app to scan the QR code below. Then enter the code generated.",
|
||||
"factorNameLabel": "A memorable name to identify this factor",
|
||||
"factorNameHint": "Use an easy-to-remember name to easily identify this factor in the future. Ex. iPhone 14",
|
||||
"factorNameSubmitLabel": "Set factor name",
|
||||
"unenrollTooltip": "Unenroll this factor",
|
||||
"unenrollingFactor": "Unenrolling factor...",
|
||||
"unenrollFactorSuccess": "Factor successfully unenrolled",
|
||||
"unenrollFactorError": "Unenrolling factor failed",
|
||||
"factorsListError": "Error loading factors list",
|
||||
"factorsListErrorDescription": "Sorry, we couldn't load the factors list. Please try again.",
|
||||
"factorName": "Factor Name",
|
||||
"factorType": "Type",
|
||||
"factorStatus": "Status",
|
||||
"mfaEnabledSuccessTitle": "Multi-Factor authentication is enabled",
|
||||
"mfaEnabledSuccessDescription": "Congratulations! You have successfully enrolled in the multi factor authentication process. You will now be able to access your account with a combination of your password and an authentication code sent to your phone number.",
|
||||
"verificationCode": "Verification Code",
|
||||
"addEmailAddress": "Add Email address",
|
||||
"verifyActivationCodeDescription": "Enter the 6-digit code generated by your authenticator app in the field above",
|
||||
"loadingFactors": "Loading factors...",
|
||||
"enableMfaFactor": "Enable Factor",
|
||||
"disableMfaFactor": "Disable Factor",
|
||||
"qrCodeErrorHeading": "QR Code Error",
|
||||
"qrCodeErrorDescription": "Sorry, we weren't able to generate the QR code",
|
||||
"multiFactorSetupSuccess": "Factor successfully enrolled",
|
||||
"submitVerificationCode": "Submit Verification Code",
|
||||
"mfaEnabledSuccessAlert": "Multi-Factor authentication is enabled",
|
||||
"verifyingCode": "Verifying code...",
|
||||
"invalidVerificationCodeHeading": "Invalid Verification Code",
|
||||
"invalidVerificationCodeDescription": "The verification code you entered is invalid. Please try again.",
|
||||
"unenrollFactorModalHeading": "Unenroll Factor",
|
||||
"unenrollFactorModalDescription": "You're about to unenroll this factor. You will not be able to use it to login to your account.",
|
||||
"unenrollFactorModalBody": "You're about to unenroll this factor. You will not be able to use it to login to your account.",
|
||||
"unenrollFactorModalButtonLabel": "Yes, unenroll factor",
|
||||
"selectFactor": "Choose a factor to verify your identity",
|
||||
"disableMfa": "Disable Multi-Factor Authentication",
|
||||
"disableMfaButtonLabel": "Disable MFA",
|
||||
"confirmDisableMfaButtonLabel": "Yes, disable MFA",
|
||||
"disablingMfa": "Disabling Multi-Factor Authentication. Please wait...",
|
||||
"disableMfaSuccess": "Multi-Factor Authentication successfully disabled",
|
||||
"disableMfaError": "Sorry, we encountered an error. MFA has not been disabled.",
|
||||
"sendingEmailVerificationLink": "Sending Email...",
|
||||
"sendEmailVerificationLinkSuccess": "Verification link successfully sent",
|
||||
"sendEmailVerificationLinkError": "Sorry, we weren't able to send you the email",
|
||||
"sendVerificationLinkSubmitLabel": "Send Verification Link",
|
||||
"sendVerificationLinkSuccessLabel": "Email sent! Check your Inbox",
|
||||
"verifyEmailAlertHeading": "Please verify your email to enable MFA",
|
||||
"verificationLinkAlertDescription": "Your email is not yet verified. Please verify your email to be able to set up Multi-Factor Authentication.",
|
||||
"authFactorName": "Factor Name (optional)",
|
||||
"authFactorNameHint": "Assign a name that helps you remember the phone number used",
|
||||
"loadingUser": "Loading user details. Please wait...",
|
||||
"linkPhoneNumber": "Link Phone Number",
|
||||
"dangerZone": "Danger Zone",
|
||||
"dangerZoneDescription": "Some actions cannot be undone. Please be careful.",
|
||||
"deleteAccount": "Delete your Account",
|
||||
"deletingAccount": "Deleting account. Please wait...",
|
||||
"deleteAccountDescription": "This will delete your account and the accounts you own. Furthermore, we will immediately cancel any active subscriptions. This action cannot be undone.",
|
||||
"deleteProfileConfirmationInputLabel": "Type DELETE to confirm",
|
||||
"deleteAccountErrorHeading": "Sorry, we couldn't delete your account",
|
||||
"needsReauthentication": "Reauthentication Required",
|
||||
"needsReauthenticationDescription": "You need to reauthenticate to change your password. Please sign out and sign in again to change your password.",
|
||||
"language": "Language",
|
||||
"languageDescription": "Choose your preferred language",
|
||||
"noTeamsYet": "You don't have any teams yet.",
|
||||
"createTeam": "Create a team to get started.",
|
||||
"createTeamButtonLabel": "Create a Team",
|
||||
"accountTabLabel": "Konto seaded",
|
||||
"accountTabDescription": "Halda oma konto seadeid ja e-posti eelistusi.",
|
||||
"preferencesTabLabel": "Eelistused",
|
||||
"preferencesTabDescription": "Halda oma eelistusi.",
|
||||
"securityTabLabel": "Turvalisus",
|
||||
"securityTabDescription": "Kaitse oma kontot.",
|
||||
"homePage": "Avaleht",
|
||||
"billingTab": "Arveldamine",
|
||||
"settingsTab": "Seaded",
|
||||
"multiFactorAuth": "Mitmefaktoriline autentimine",
|
||||
"multiFactorAuthDescription": "Sea üles mitmefaktoriline autentimine, et oma kontot rohkem turvata",
|
||||
"updateProfileSuccess": "Profiil edukalt uuendatud",
|
||||
"updateProfileError": "Ilmnes viga. Palun proovi uuesti",
|
||||
"updatePasswordSuccess": "Parooli uuendamine õnnestus",
|
||||
"updatePasswordSuccessMessage": "Sinu parool on edukalt uuendatud!",
|
||||
"updatePasswordError": "Ilmnes viga. Palun proovi uuesti",
|
||||
"updatePasswordLoading": "Parooli uuendamine...",
|
||||
"updateProfileLoading": "Profiili uuendamine...",
|
||||
"name": "Sinu nimi",
|
||||
"nameDescription": "Uuenda oma nime, mis kuvatakse profiilil",
|
||||
"emailLabel": "E-posti aadress",
|
||||
"accountImage": "Sinu profiilipilt",
|
||||
"accountImageDescription": "Vali foto, mida soovid profiilipildina üles laadida.",
|
||||
"profilePictureHeading": "Laadi üles profiilipilt",
|
||||
"profilePictureSubheading": "Vali foto, mida soovid profiilipildina üles laadida.",
|
||||
"updateProfileSubmitLabel": "Uuenda profiili",
|
||||
"updatePasswordCardTitle": "Uuenda oma parool",
|
||||
"updatePasswordCardDescription": "Uuenda oma parooli, et hoida oma konto turvaline.",
|
||||
"currentPassword": "Praegune parool",
|
||||
"newPassword": "Uus parool",
|
||||
"repeatPassword": "Korda uut parooli",
|
||||
"repeatPasswordDescription": "Palun korda oma uus parool, et seda kinnitada",
|
||||
"yourPassword": "Sinu parool",
|
||||
"updatePasswordSubmitLabel": "Uuenda parooli",
|
||||
"updateEmailCardTitle": "Uuenda oma e-posti",
|
||||
"updateEmailCardDescription": "Uuenda e-posti aadressi, mida kasutad kontole sisselogimiseks",
|
||||
"newEmail": "Sinu uus e-post",
|
||||
"repeatEmail": "Korda e-posti",
|
||||
"updateEmailSubmitLabel": "Uuenda e-posti aadressi",
|
||||
"updateEmailSuccess": "E-posti uuendamine õnnestus",
|
||||
"updateEmailSuccessMessage": "Saadame sulle kinnituskirja uue e-posti aadressi kinnitamiseks. Palun vaata oma postkasti ja klõpsa lingil.",
|
||||
"updateEmailLoading": "E-posti uuendamine...",
|
||||
"updateEmailError": "E-posti ei uuendatud. Palun proovi uuesti",
|
||||
"passwordNotMatching": "Paroolid ei ühti. Veendu, et kasutad õiget parooli",
|
||||
"emailNotMatching": "E-postid ei ühti. Veendu, et kasutad õiget e-posti",
|
||||
"passwordNotChanged": "Sinu parool ei ole muutunud",
|
||||
"emailsNotMatching": "E-postid ei ühti. Veendu, et kasutad õiget e-posti",
|
||||
"cannotUpdatePassword": "Sa ei saa oma parooli uuendada, kuna sinu kontot ei ole lingitud ühegi parooliga.",
|
||||
"setupMfaButtonLabel": "Sea uus faktor",
|
||||
"multiFactorSetupErrorHeading": "Seadistamine ebaõnnestus",
|
||||
"multiFactorSetupErrorDescription": "Vabandame, tekkis viga faktori seadistamisel. Palun proovi uuesti.",
|
||||
"multiFactorAuthHeading": "Turvake oma konto mitmefaktorilise autentimisega",
|
||||
"multiFactorModalHeading": "Kasuta oma autentimisrakendust QR-koodi skannimiseks. Seejärel sisesta genereeritud kood.",
|
||||
"factorNameLabel": "Meeldejääv nimi faktori tuvastamiseks",
|
||||
"factorNameHint": "Kasuta lihtsat nime, et hiljem seda faktorit kergesti tuvastada. Nt iPhone 14",
|
||||
"factorNameSubmitLabel": "Määra faktori nimi",
|
||||
"unenrollTooltip": "Tühista selle faktori registreerimine",
|
||||
"unenrollingFactor": "Faktori registreerimine tühistatakse...",
|
||||
"unenrollFactorSuccess": "Faktor edukalt tühistatud",
|
||||
"unenrollFactorError": "Faktori tühistamine ebaõnnestus",
|
||||
"factorsListError": "Faktorite nimekirja laadimisel tekkis viga",
|
||||
"factorsListErrorDescription": "Vabandame, ei õnnestunud faktorite nimekirja laadida. Palun proovi uuesti.",
|
||||
"factorName": "Faktori nimi",
|
||||
"factorType": "Tüüp",
|
||||
"factorStatus": "Staatus",
|
||||
"mfaEnabledSuccessTitle": "Mitmefaktoriline autentimine on aktiveeritud",
|
||||
"mfaEnabledSuccessDescription": "Palju õnne! Sa oled edukalt registreeritud mitmefaktorilise autentimise protsessi. Nüüd pääsed oma kontole parooli ja autentimiskoodi abil.",
|
||||
"verificationCode": "Kinnituskood",
|
||||
"addEmailAddress": "Lisa e-posti aadress",
|
||||
"verifyActivationCodeDescription": "Sisesta 6-kohaline kood, mille sinu autentimisrakendus genereeris",
|
||||
"loadingFactors": "Faktorite laadimine...",
|
||||
"enableMfaFactor": "Luba faktor",
|
||||
"disableMfaFactor": "Keela faktor",
|
||||
"qrCodeErrorHeading": "QR-koodi viga",
|
||||
"qrCodeErrorDescription": "Vabandame, QR-koodi genereerimine ebaõnnestus",
|
||||
"multiFactorSetupSuccess": "Faktor edukalt registreeritud",
|
||||
"submitVerificationCode": "Esita kinnituskood",
|
||||
"mfaEnabledSuccessAlert": "Mitmefaktoriline autentimine on aktiveeritud",
|
||||
"verifyingCode": "Koodi kontrollimine...",
|
||||
"invalidVerificationCodeHeading": "Vale kinnituskood",
|
||||
"invalidVerificationCodeDescription": "Sisestatud kinnituskood ei kehti. Palun proovi uuesti.",
|
||||
"unenrollFactorModalHeading": "Tühista faktori registreerimine",
|
||||
"unenrollFactorModalDescription": "Sa oled tühistamas selle faktori registreerimist. Sa ei saa seda enam kontole sisselogimiseks kasutada.",
|
||||
"unenrollFactorModalBody": "Sa oled tühistamas selle faktori registreerimist. Sa ei saa seda enam kontole sisselogimiseks kasutada.",
|
||||
"unenrollFactorModalButtonLabel": "Jah, tühista faktor",
|
||||
"selectFactor": "Vali faktor, et tuvastada oma identiteet",
|
||||
"disableMfa": "Keela mitmefaktoriline autentimine",
|
||||
"disableMfaButtonLabel": "Keela MFA",
|
||||
"confirmDisableMfaButtonLabel": "Jah, keela MFA",
|
||||
"disablingMfa": "Mitmefaktoriline autentimine keelatakse. Palun oota...",
|
||||
"disableMfaSuccess": "Mitmefaktoriline autentimine edukalt keelatud",
|
||||
"disableMfaError": "Vabandame, tekkis viga. MFA ei ole keelatud.",
|
||||
"sendingEmailVerificationLink": "E-kirja saatmine...",
|
||||
"sendEmailVerificationLinkSuccess": "Kinnituse link edukalt saadetud",
|
||||
"sendEmailVerificationLinkError": "Vabandame, e-kirja saatmine ebaõnnestus",
|
||||
"sendVerificationLinkSubmitLabel": "Saada kinnituse link",
|
||||
"sendVerificationLinkSuccessLabel": "E-post saadetud! Vaata oma postkasti",
|
||||
"verifyEmailAlertHeading": "Palun kinnita oma e-post, et lubada MFA",
|
||||
"verificationLinkAlertDescription": "Sinu e-post ei ole veel kinnitatud. Palun kinnita e-post, et saaksid mitmefaktorilise autentimise seadistada.",
|
||||
"authFactorName": "Faktori nimi (valikuline)",
|
||||
"authFactorNameHint": "Määra nimi, mis aitab meenutada kasutatud telefoninumbrit",
|
||||
"loadingUser": "Kasutaja andmete laadimine. Palun oota...",
|
||||
"linkPhoneNumber": "Seosta telefoninumber",
|
||||
"dangerZone": "Ohtlik tsoon",
|
||||
"dangerZoneDescription": "Mõnda toimingut ei saa tagasi võtta. Ole ettevaatlik.",
|
||||
"deleteAccount": "Kustuta oma konto",
|
||||
"deletingAccount": "Konto kustutamine. Palun oota...",
|
||||
"deleteAccountDescription": "See kustutab sinu konto ja kõik kontod, mille omanik sa oled. Samuti tühistatakse kohe kõik aktiivsed tellimused. Seda toimingut ei saa tagasi võtta.",
|
||||
"deleteProfileConfirmationInputLabel": "Sisesta KUSTUTA, et kinnitada",
|
||||
"deleteAccountErrorHeading": "Vabandame, me ei saanud sinu kontot kustutada",
|
||||
"needsReauthentication": "Taastõendamine vajalik",
|
||||
"needsReauthenticationDescription": "Sa pead uuesti autentima, et muuta oma parooli. Palun logi välja ja seejärel sisse, et parooli muuta.",
|
||||
"language": "Keel",
|
||||
"languageDescription": "Vali eelistatud keel",
|
||||
"noTeamsYet": "Sul ei ole veel meeskondi.",
|
||||
"createTeam": "Loo meeskond alustamiseks.",
|
||||
"createTeamButtonLabel": "Loo meeskond",
|
||||
"createCompanyAccount": "Loo ettevõtte konto",
|
||||
"requestCompanyAccount": {
|
||||
"title": "Ettevõtte andmed",
|
||||
"description": "Pakkumise saamiseks palun sisesta ettevõtte andmed millega MedReport kasutada kavatsed.",
|
||||
"description": "Pakkumise saamiseks palun sisesta ettevõtte andmed, millega MedReporti kasutada kavatsed.",
|
||||
"button": "Küsi pakkumist",
|
||||
"successTitle": "Päring edukalt saadetud!",
|
||||
"successDescription": "Saadame teile esimesel võimalusel vastuse",
|
||||
"successButton": "Tagasi kodulehele"
|
||||
"successDescription": "Vastame sulle esimesel võimalusel",
|
||||
"successButton": "Tagasi avalehele"
|
||||
},
|
||||
"updateAccount": {
|
||||
"title": "Isikuandmed",
|
||||
@@ -129,7 +133,7 @@
|
||||
"userConsentUrlTitle": "Vaata isikuandmete töötlemise põhimõtteid"
|
||||
},
|
||||
"consentModal": {
|
||||
"title": "Enne toimetama hakkamist",
|
||||
"title": "Enne alustamist",
|
||||
"description": "Kas annad nõusoleku, et sinu terviseandmeid kasutatakse anonüümselt tööandja statistikas? Andmed jäävad isikustamata ja aitavad ettevõttel töötajate tervist paremini toetada.",
|
||||
"reject": "Ei anna nõusolekut",
|
||||
"accept": "Annan nõusoleku"
|
||||
@@ -152,5 +156,14 @@
|
||||
"updatePreferredLocaleSuccess": "Eelistatud keel uuendatud",
|
||||
"updatePreferredLocaleError": "Eelistatud keele uuendamine ei õnnestunud",
|
||||
"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"
|
||||
}
|
||||
@@ -1,90 +1,90 @@
|
||||
{
|
||||
"signUpHeading": "Create an account",
|
||||
"signUp": "Sign Up",
|
||||
"signUpSubheading": "Fill the form below to create an account.",
|
||||
"signInHeading": "Sign in to your account",
|
||||
"signInSubheading": "Welcome back! Please enter your details",
|
||||
"signIn": "Sign In",
|
||||
"getStarted": "Get started",
|
||||
"updatePassword": "Update Password",
|
||||
"signOut": "Sign out",
|
||||
"signingIn": "Signing in...",
|
||||
"signingUp": "Signing up...",
|
||||
"doNotHaveAccountYet": "Do not have an account yet?",
|
||||
"alreadyHaveAnAccount": "Already have an account?",
|
||||
"signUpToAcceptInvite": "Please sign in/up to accept the invite",
|
||||
"clickToAcceptAs": "Click the button below to accept the invite with as <b>{{email}}</b>",
|
||||
"acceptInvite": "Accept invite",
|
||||
"acceptingInvite": "Accepting Invite...",
|
||||
"acceptInviteSuccess": "Invite successfully accepted",
|
||||
"acceptInviteError": "Error encountered while accepting invite",
|
||||
"acceptInviteWithDifferentAccount": "Want to accept the invite with a different account?",
|
||||
"alreadyHaveAccountStatement": "I already have an account, I want to sign in instead",
|
||||
"doNotHaveAccountStatement": "I do not have an account, I want to sign up instead",
|
||||
"signInWithProvider": "Sign in with {{provider}}",
|
||||
"signInWithPhoneNumber": "Sign in with Phone Number",
|
||||
"signInWithEmail": "Sign in with Email",
|
||||
"signUpWithEmail": "Sign up with Email",
|
||||
"passwordHint": "Ensure it's at least 8 characters",
|
||||
"repeatPasswordHint": "Type your password again",
|
||||
"repeatPassword": "Repeat password",
|
||||
"passwordForgottenQuestion": "Password forgotten?",
|
||||
"passwordResetLabel": "Reset Password",
|
||||
"passwordResetSubheading": "Enter your email address below. You will receive a link to reset your password.",
|
||||
"passwordResetSuccessMessage": "Check your Inbox! We emailed you a link for resetting your Password.",
|
||||
"passwordRecoveredQuestion": "Password recovered?",
|
||||
"passwordLengthError": "Please provide a password with at least 6 characters",
|
||||
"sendEmailLink": "Send Email Link",
|
||||
"sendingEmailLink": "Sending Email Link...",
|
||||
"sendLinkSuccessDescription": "Check your email, we just sent you a link. Follow the link to sign in.",
|
||||
"sendLinkSuccess": "We sent you a link by email",
|
||||
"sendLinkSuccessToast": "Link successfully sent",
|
||||
"getNewLink": "Get a new link",
|
||||
"verifyCodeHeading": "Verify your account",
|
||||
"verificationCode": "Verification Code",
|
||||
"verificationCodeHint": "Enter the code we sent you by SMS",
|
||||
"verificationCodeSubmitButtonLabel": "Submit Verification Code",
|
||||
"sendingMfaCode": "Sending Verification Code...",
|
||||
"verifyingMfaCode": "Verifying code...",
|
||||
"sendMfaCodeError": "Sorry, we couldn't send you a verification code",
|
||||
"verifyMfaCodeSuccess": "Code verified! Signing you in...",
|
||||
"verifyMfaCodeError": "Ops! It looks like the code is not correct",
|
||||
"reauthenticate": "Reauthenticate",
|
||||
"reauthenticateDescription": "For security reasons, we need you to re-authenticate",
|
||||
"errorAlertHeading": "Sorry, we could not authenticate you",
|
||||
"emailConfirmationAlertHeading": "We sent you a confirmation email.",
|
||||
"emailConfirmationAlertBody": "Welcome! Please check your email and click the link to verify your account.",
|
||||
"resendLink": "Resend link",
|
||||
"resendLinkSuccessDescription": "We sent you a new link to your email! Follow the link to sign in.",
|
||||
"resendLinkSuccess": "Check your email!",
|
||||
"authenticationErrorAlertHeading": "Authentication Error",
|
||||
"authenticationErrorAlertBody": "Sorry, we could not authenticate you. Please try again.",
|
||||
"sendEmailCode": "Get code to your Email",
|
||||
"sendingEmailCode": "Sending code...",
|
||||
"resetPasswordError": "Sorry, we could not reset your password. Please try again.",
|
||||
"emailPlaceholder": "your@email.com",
|
||||
"inviteAlertHeading": "You have been invited to join a company",
|
||||
"inviteAlertBody": "Please sign in or sign up to accept the invite and join the company.",
|
||||
"acceptTermsAndConditions": "I accept the <TermsOfServiceLink /> and <PrivacyPolicyLink />",
|
||||
"termsOfService": "Terms of Service",
|
||||
"privacyPolicy": "Privacy Policy",
|
||||
"orContinueWith": "Or continue with",
|
||||
"redirecting": "You're in! Please wait...",
|
||||
"signUpHeading": "Loo konto",
|
||||
"signUp": "Loo konto",
|
||||
"signUpSubheading": "Täida allolev vorm, et luua konto.",
|
||||
"signInHeading": "Logi oma kontole sisse",
|
||||
"signInSubheading": "Tere tulemast tagasi! Palun sisesta oma andmed",
|
||||
"signIn": "Logi sisse",
|
||||
"getStarted": "Alusta",
|
||||
"updatePassword": "Uuenda parooli",
|
||||
"signOut": "Logi välja",
|
||||
"signingIn": "Sisselogimine...",
|
||||
"signingUp": "Registreerimine...",
|
||||
"doNotHaveAccountYet": "Kas sul pole veel kontot?",
|
||||
"alreadyHaveAnAccount": "Kas sul on juba konto?",
|
||||
"signUpToAcceptInvite": "Palun logi sisse/registreeru, et kutse vastu võtta",
|
||||
"clickToAcceptAs": "Klõpsa alloleval nupul, et võtta kutse vastu kui <b>{{email}}</b>",
|
||||
"acceptInvite": "Võta kutse vastu",
|
||||
"acceptingInvite": "Kutset vastu võttes...",
|
||||
"acceptInviteSuccess": "Kutse on edukalt vastu võetud",
|
||||
"acceptInviteError": "Kutse vastuvõtmisel tekkis viga",
|
||||
"acceptInviteWithDifferentAccount": "Soovid kutse vastu võtta teise kontoga?",
|
||||
"alreadyHaveAccountStatement": "Mul on juba konto, ma tahan sisse logida",
|
||||
"doNotHaveAccountStatement": "Mul pole kontot, ma tahan registreeruda",
|
||||
"signInWithProvider": "Logi sisse teenusega {{provider}}",
|
||||
"signInWithPhoneNumber": "Logi sisse telefoninumbriga",
|
||||
"signInWithEmail": "Logi sisse e-posti aadressiga",
|
||||
"signUpWithEmail": "Registreeru e-posti aadressiga",
|
||||
"passwordHint": "Veendu, et see oleks vähemalt 8 tähemärki pikk",
|
||||
"repeatPasswordHint": "Sisesta oma parool uuesti",
|
||||
"repeatPassword": "Korda parooli",
|
||||
"passwordForgottenQuestion": "Unustasid parooli?",
|
||||
"passwordResetLabel": "Taasta parool",
|
||||
"passwordResetSubheading": "Sisesta oma e-posti aadress alla. Saadame sulle lingi parooli lähtestamiseks.",
|
||||
"passwordResetSuccessMessage": "Kontrolli oma postkasti! Saatsime sulle parooli lähtestamise lingi.",
|
||||
"passwordRecoveredQuestion": "Kas parool on taastatud?",
|
||||
"passwordLengthError": "Palun sisesta parool, mis on vähemalt 6 tähemärki pikk",
|
||||
"sendEmailLink": "Saada e-posti link",
|
||||
"sendingEmailLink": "E-posti lingi saatmine...",
|
||||
"sendLinkSuccessDescription": "Kontrolli oma e-posti, just saatsime sulle lingi. Järgi linki, et sisse logida.",
|
||||
"sendLinkSuccess": "Saadame sulle lingi e-posti teel",
|
||||
"sendLinkSuccessToast": "Link edukalt saadetud",
|
||||
"getNewLink": "Hangi uus link",
|
||||
"verifyCodeHeading": "Kinnita oma konto",
|
||||
"verificationCode": "Kinnituskood",
|
||||
"verificationCodeHint": "Sisesta SMS-iga saadetud kood",
|
||||
"verificationCodeSubmitButtonLabel": "Sisesta kinnituskood",
|
||||
"sendingMfaCode": "Kinnituskoodi saatmine...",
|
||||
"verifyingMfaCode": "Koodi kontrollimine...",
|
||||
"sendMfaCodeError": "Vabandust, meil ei õnnestunud saata kinnituskoodi",
|
||||
"verifyMfaCodeSuccess": "Kood kinnitatud! Sisselogimine...",
|
||||
"verifyMfaCodeError": "Ups! Paistab, et kood ei ole õige",
|
||||
"reauthenticate": "Autentige uuesti",
|
||||
"reauthenticateDescription": "Turvalisuse huvides peame teid uuesti autentima",
|
||||
"errorAlertHeading": "Vabandust, me ei saanud sind autentida",
|
||||
"emailConfirmationAlertHeading": "Saatsime sulle kinnituskirja e-posti teel.",
|
||||
"emailConfirmationAlertBody": "Tere tulemast! Palun kontrolli oma e-posti ja klõpsa lingil, et konto kinnitada.",
|
||||
"resendLink": "Saada link uuesti",
|
||||
"resendLinkSuccessDescription": "Saime sulle saata uue lingi! Järgi linki, et sisse logida.",
|
||||
"resendLinkSuccess": "Kontrolli oma e-posti!",
|
||||
"authenticationErrorAlertHeading": "Autentimise viga",
|
||||
"authenticationErrorAlertBody": "Vabandust, me ei saanud sind autentida. Palun proovi uuesti.",
|
||||
"sendEmailCode": "Hangi kood e-posti teel",
|
||||
"sendingEmailCode": "Koodi saatmine...",
|
||||
"resetPasswordError": "Vabandust, me ei saanud parooli lähtestada. Palun proovi uuesti.",
|
||||
"emailPlaceholder": "sinu@email.com",
|
||||
"inviteAlertHeading": "Sind on kutsutud ettevõttega liituma",
|
||||
"inviteAlertBody": "Palun logi sisse või registreeru, et kutse vastu võtta ja ettevõttega liituda.",
|
||||
"acceptTermsAndConditions": "Ma nõustun <TermsOfServiceLink /> ja <PrivacyPolicyLink />",
|
||||
"termsOfService": "Kasutustingimused",
|
||||
"privacyPolicy": "Privaatsuspoliitika",
|
||||
"orContinueWith": "Või jätka koos",
|
||||
"redirecting": "Oled sees! Palun oota...",
|
||||
"errors": {
|
||||
"Invalid login credentials": "The credentials entered are invalid",
|
||||
"User already registered": "This credential is already in use. Please try with another one.",
|
||||
"Email not confirmed": "Please confirm your email address before signing in",
|
||||
"default": "We have encountered an error. Please ensure you have a working internet connection and try again",
|
||||
"generic": "Sorry, we weren't able to authenticate you. Please try again.",
|
||||
"link": "Sorry, we encountered an error while sending your link. Please try again.",
|
||||
"codeVerifierMismatch": "It looks like you're trying to sign in using a different browser than the one you used to request the sign in link. Please try again using the same browser.",
|
||||
"minPasswordLength": "Password must be at least 8 characters long",
|
||||
"passwordsDoNotMatch": "The passwords do not match",
|
||||
"minPasswordNumbers": "Password must contain at least one number",
|
||||
"minPasswordSpecialChars": "Password must contain at least one special character",
|
||||
"uppercasePassword": "Password must contain at least one uppercase letter",
|
||||
"insufficient_aal": "Please sign-in with your current multi-factor authentication to perform this action",
|
||||
"otp_expired": "The email link has expired. Please try again.",
|
||||
"same_password": "The password cannot be the same as the current password"
|
||||
"Invalid login credentials": "Sisestatud andmed on valed",
|
||||
"User already registered": "See konto on juba kasutusel. Palun proovi teisega.",
|
||||
"Email not confirmed": "Palun kinnita oma e-posti aadress enne sisselogimist",
|
||||
"default": "Tekkis viga. Palun veendu, et sul on töötav internetiühendus, ja proovi uuesti",
|
||||
"generic": "Vabandust, me ei saanud sind autentida. Palun proovi uuesti.",
|
||||
"link": "Vabandust, lingi saatmisel tekkis viga. Palun proovi uuesti.",
|
||||
"codeVerifierMismatch": "Paistab, et proovid sisse logida teises brauseris kui see, millest lingi taotlesid. Palun proovi uuesti sama brauseriga.",
|
||||
"minPasswordLength": "Parool peab olema vähemalt 8 tähemärki pikk",
|
||||
"passwordsDoNotMatch": "Paroolid ei kattu",
|
||||
"minPasswordNumbers": "Parool peab sisaldama vähemalt ühte numbrit",
|
||||
"minPasswordSpecialChars": "Parool peab sisaldama vähemalt ühte erimärki",
|
||||
"uppercasePassword": "Parool peab sisaldama vähemalt ühte suurtähte",
|
||||
"insufficient_aal": "Palun logi sisse oma mitmeastmelise autentimisega, et seda toimingut teha",
|
||||
"otp_expired": "E-posti link on aegunud. Palun proovi uuesti.",
|
||||
"same_password": "Parool ei tohi olla sama, mis praegune parool"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"emptyCartMessage": "Sinu ostukorv on tühi",
|
||||
"emptyCartMessageDescription": "Lisa tooteid ostukorvi, et jätkata.",
|
||||
"subtotal": "Vahesumma",
|
||||
"promotionsTotal": "Soodustuse summa",
|
||||
"total": "Summa",
|
||||
"table": {
|
||||
"item": "Toode",
|
||||
@@ -28,7 +29,13 @@
|
||||
"label": "Lisa promo kood",
|
||||
"apply": "Rakenda",
|
||||
"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": {
|
||||
"synlabAnalyses": {
|
||||
|
||||
@@ -1,61 +1,61 @@
|
||||
{
|
||||
"homeTabLabel": "Home",
|
||||
"homeTabDescription": "Welcome to your home page",
|
||||
"accountMembers": "Company Members",
|
||||
"membersTabDescription": "Here you can manage the members of your company.",
|
||||
"billingTabLabel": "Billing",
|
||||
"billingTabDescription": "Manage your billing and subscription",
|
||||
"dashboardTabLabel": "Dashboard",
|
||||
"settingsTabLabel": "Settings",
|
||||
"profileSettingsTabLabel": "Profile",
|
||||
"subscriptionSettingsTabLabel": "Subscription",
|
||||
"dashboardTabDescription": "An overview of your account's activity and performance across all your projects.",
|
||||
"settingsTabDescription": "Manage your settings and preferences.",
|
||||
"emailAddress": "Email Address",
|
||||
"password": "Password",
|
||||
"modalConfirmationQuestion": "Are you sure you want to continue?",
|
||||
"imageInputLabel": "Click here to upload an image",
|
||||
"cancel": "Cancel",
|
||||
"clear": "Clear",
|
||||
"homeTabLabel": "Avaleht",
|
||||
"homeTabDescription": "Tere tulemast sinu avalehele",
|
||||
"accountMembers": "Ettevõtte liikmed",
|
||||
"membersTabDescription": "Siit saad hallata oma ettevõtte liikmeid.",
|
||||
"billingTabLabel": "Arveldamine",
|
||||
"billingTabDescription": "Halda oma arveldamist ja tellimusi",
|
||||
"dashboardTabLabel": "Ülevaade",
|
||||
"settingsTabLabel": "Seaded",
|
||||
"profileSettingsTabLabel": "Profiil",
|
||||
"subscriptionSettingsTabLabel": "Tellimus",
|
||||
"dashboardTabDescription": "Ülevaade sinu konto tegevusest ja tulemuste kohta kõigis projektides.",
|
||||
"settingsTabDescription": "Halda oma seadeid ja eelistusi.",
|
||||
"emailAddress": "E-posti aadress",
|
||||
"password": "Parool",
|
||||
"modalConfirmationQuestion": "Oled sa kindel, et soovid jätkata?",
|
||||
"imageInputLabel": "Klikka siia, et üles laadida pilt",
|
||||
"cancel": "Tühista",
|
||||
"clear": "Kustuta",
|
||||
"close": "Sulge",
|
||||
"notFound": "Not Found",
|
||||
"backToHomePage": "Back to Home Page",
|
||||
"notFound": "Ei leitud",
|
||||
"backToHomePage": "Tagasi avalehele",
|
||||
"goBack": "Tagasi",
|
||||
"genericServerError": "Sorry, something went wrong.",
|
||||
"genericServerErrorHeading": "Sorry, something went wrong while processing your request. Please contact us if the issue persists.",
|
||||
"pageNotFound": "Sorry, this page does not exist.",
|
||||
"pageNotFoundSubHeading": "Apologies, the page you were looking for was not found",
|
||||
"genericError": "Sorry, something went wrong.",
|
||||
"genericErrorSubHeading": "Apologies, an error occurred while processing your request. Please contact us if the issue persists.",
|
||||
"anonymousUser": "Anonymous",
|
||||
"tryAgain": "Try Again",
|
||||
"theme": "Theme",
|
||||
"lightTheme": "Light",
|
||||
"darkTheme": "Dark",
|
||||
"systemTheme": "System",
|
||||
"expandSidebar": "Expand Sidebar",
|
||||
"collapseSidebar": "Collapse Sidebar",
|
||||
"documentation": "Documentation",
|
||||
"genericServerError": "Vabandame, midagi läks valesti.",
|
||||
"genericServerErrorHeading": "Vabandame, midagi läks valesti teie päringu töötlemisel. Palun võtke meiega ühendust, kui probleem püsib.",
|
||||
"pageNotFound": "Vabandame, seda lehte ei eksisteeri.",
|
||||
"pageNotFoundSubHeading": "Vabandame, lehte, mida otsisite, ei leitud",
|
||||
"genericError": "Vabandame, midagi läks valesti.",
|
||||
"genericErrorSubHeading": "Vabandame, ilmnes viga teie päringu töötlemisel. Palun võtke meiega ühendust, kui probleem püsib.",
|
||||
"anonymousUser": "Anonüümne",
|
||||
"tryAgain": "Proovi uuesti",
|
||||
"theme": "Teema",
|
||||
"lightTheme": "Hele",
|
||||
"darkTheme": "Tume",
|
||||
"systemTheme": "Süsteem",
|
||||
"expandSidebar": "Laienda külgriba",
|
||||
"collapseSidebar": "Kokkuvoldi külgriba",
|
||||
"documentation": "Dokumentatsioon",
|
||||
"getStarted": "Alusta!",
|
||||
"getStartedWithPlan": "Get Started with {{plan}}",
|
||||
"retry": "Retry",
|
||||
"contactUs": "Contact Us",
|
||||
"loading": "Loading. Please wait...",
|
||||
"yourAccounts": "Your Accounts",
|
||||
"continue": "Continue",
|
||||
"skip": "Skip",
|
||||
"signedInAs": "Signed in as",
|
||||
"getStartedWithPlan": "Alusta plaaniga {{plan}}",
|
||||
"retry": "Proovi uuesti",
|
||||
"contactUs": "Võta meiega ühendust",
|
||||
"loading": "Laadimine. Palun oota...",
|
||||
"yourAccounts": "Sinu kontod",
|
||||
"continue": "Jätka",
|
||||
"skip": "Jäta vahele",
|
||||
"signedInAs": "Sisselogitud kasutajana",
|
||||
"pageOfPages": "Leht {{page}} / {{total}}",
|
||||
"noData": "Andmed puuduvad",
|
||||
"pageNotFoundHeading": "Ouch! :|",
|
||||
"errorPageHeading": "Ouch! :|",
|
||||
"notifications": "Notifications",
|
||||
"noNotifications": "No notifications",
|
||||
"justNow": "Just now",
|
||||
"newVersionAvailable": "New version available",
|
||||
"newVersionAvailableDescription": "A new version of the app is available. It is recommended to refresh the page to get the latest updates and avoid any issues.",
|
||||
"newVersionSubmitButton": "Reload and Update",
|
||||
"back": "Back",
|
||||
"noData": "Andmeid puudub",
|
||||
"pageNotFoundHeading": "Ups! :|",
|
||||
"errorPageHeading": "Ups! :|",
|
||||
"notifications": "Teavitused",
|
||||
"noNotifications": "Teavitusi pole",
|
||||
"justNow": "Just nüüd",
|
||||
"newVersionAvailable": "Uus versioon saadaval",
|
||||
"newVersionAvailableDescription": "Rakenduse uus versioon on saadaval. Soovitame lehe värskendada, et saada uusimad uuendused ja vältida probleeme.",
|
||||
"newVersionSubmitButton": "Värskenda ja uuenda",
|
||||
"back": "Tagasi",
|
||||
"welcome": "Tere tulemast",
|
||||
"shoppingCart": "Ostukorv",
|
||||
"shoppingCartCount": "Ostukorv ({{count}})",
|
||||
@@ -63,10 +63,10 @@
|
||||
"myActions": "Minu toimingud",
|
||||
"healthPackageComparison": {
|
||||
"label": "Tervisepakettide võrdlus",
|
||||
"description": "Alljärgnevalt on antud eelinfo (sugu, vanus ja kehamassiindeksi) põhjal tehtud personalne terviseauditi valik. Tabelis on võimalik soovitatud terviseuuringute paketile lisada üksikuid uuringuid juurde."
|
||||
"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."
|
||||
},
|
||||
"routes": {
|
||||
"home": "Home",
|
||||
"home": "Avaleht",
|
||||
"overview": "Ülevaade",
|
||||
"booking": "Broneeri aeg",
|
||||
"myOrders": "Minu tellimused",
|
||||
@@ -74,47 +74,49 @@
|
||||
"orderAnalysisPackage": "Telli analüüside pakett",
|
||||
"orderAnalysis": "Telli analüüs",
|
||||
"orderHealthAnalysis": "Telli terviseuuring",
|
||||
"account": "Account",
|
||||
"members": "Members",
|
||||
"billing": "Billing",
|
||||
"account": "Konto",
|
||||
"members": "Liikmed",
|
||||
"billing": "Arveldamine",
|
||||
"dashboard": "Ülevaade",
|
||||
"settings": "Settings",
|
||||
"profile": "Profile",
|
||||
"application": "Application",
|
||||
"pickTime": "Vali aeg"
|
||||
"settings": "Seaded",
|
||||
"profile": "Profiil",
|
||||
"application": "Rakendus",
|
||||
"pickTime": "Vali aeg",
|
||||
"preferences": "Eelistused",
|
||||
"security": "Turvalisus"
|
||||
},
|
||||
"roles": {
|
||||
"owner": {
|
||||
"label": "Admin"
|
||||
},
|
||||
"member": {
|
||||
"label": "Member"
|
||||
"label": "Liige"
|
||||
}
|
||||
},
|
||||
"otp": {
|
||||
"requestVerificationCode": "Request Verification Code",
|
||||
"requestVerificationCodeDescription": "We must verify your identity to continue with this action. We'll send a verification code to the email address {{email}}.",
|
||||
"sendingCode": "Sending Code...",
|
||||
"sendVerificationCode": "Send Verification Code",
|
||||
"enterVerificationCode": "Enter Verification Code",
|
||||
"codeSentToEmail": "We've sent a verification code to the email address {{email}}.",
|
||||
"verificationCode": "Verification Code",
|
||||
"enterCodeFromEmail": "Enter the 6-digit code we sent to your email.",
|
||||
"verifying": "Verifying...",
|
||||
"verifyCode": "Verify Code",
|
||||
"requestNewCode": "Request New Code",
|
||||
"errorSendingCode": "Error sending code. Please try again."
|
||||
"requestVerificationCode": "Palun taotle kinnituskood",
|
||||
"requestVerificationCodeDescription": "Peame sinu identiteedi kontrollima, et jätkata. Saadame koodi e-posti aadressile {{email}}.",
|
||||
"sendingCode": "Koodi saatmine...",
|
||||
"sendVerificationCode": "Saada kinnituskood",
|
||||
"enterVerificationCode": "Sisesta kinnituskood",
|
||||
"codeSentToEmail": "Oleme saatnud koodi e-posti aadressile {{email}}.",
|
||||
"verificationCode": "Kinnituskood",
|
||||
"enterCodeFromEmail": "Sisesta 6-kohaline kood, mille saatsime sinu e-posti aadressile.",
|
||||
"verifying": "Kontrollimine...",
|
||||
"verifyCode": "Kontrolli koodi",
|
||||
"requestNewCode": "Taotle uut koodi",
|
||||
"errorSendingCode": "Koodi saatmisel tekkis viga. Proovi uuesti."
|
||||
},
|
||||
"cookieBanner": {
|
||||
"title": "Hey, we use cookies 🍪",
|
||||
"description": "This website uses cookies to ensure you get the best experience on our website.",
|
||||
"reject": "Reject",
|
||||
"accept": "Accept"
|
||||
"title": "Hei, me kasutame küpsiseid 🍪",
|
||||
"description": "See veebileht kasutab küpsiseid, et tagada parim kasutuskogemus.",
|
||||
"reject": "Keela",
|
||||
"accept": "Luba"
|
||||
},
|
||||
"formField": {
|
||||
"companyName": "Ettevõtte nimi",
|
||||
"contactPerson": "Kontaktisik",
|
||||
"email": "E-mail",
|
||||
"email": "E-post",
|
||||
"phone": "Telefon",
|
||||
"firstName": "Eesnimi",
|
||||
"lastName": "Perenimi",
|
||||
@@ -127,7 +129,7 @@
|
||||
"selectDate": "Vali kuupäev"
|
||||
},
|
||||
"wallet": {
|
||||
"balance": "Sinu MedReporti konto seis",
|
||||
"balance": "Sinu MedReporti konto saldo",
|
||||
"expiredAt": "Kehtiv kuni {{expiredAt}}"
|
||||
},
|
||||
"doctor": "Arst",
|
||||
@@ -137,5 +139,8 @@
|
||||
"previous": "Eelmine",
|
||||
"next": "Järgmine",
|
||||
"invalidDataError": "Vigased andmed",
|
||||
"language": "Keel"
|
||||
}
|
||||
"language": "Keel",
|
||||
"yes": "Jah",
|
||||
"no": "Ei",
|
||||
"preferNotToAnswer": "Eelistan mitte vastata"
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"recentlyCheckedDescription": "Super, oled käinud tervist kontrollimas. Siin on sinule olulised näitajad",
|
||||
"recentlyCheckedDescription": "Siin on sinu olulisemad tervisenäitajad",
|
||||
"respondToQuestion": "Vasta küsimusele",
|
||||
"gender": "Sugu",
|
||||
"male": "Mees",
|
||||
|
||||
7
public/locales/et/error.json
Normal file
7
public/locales/et/error.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"invalidNumber": "Vigane arv",
|
||||
"invalidEmail": "Vigane email",
|
||||
"tooShort": "Liiga lühike sisend",
|
||||
"tooLong": "Liiga pikk sisend",
|
||||
"invalidPhone": "Vigane telefoninumber"
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user