@@ -58,5 +58,5 @@ export default async function syncAnalysisResults() {
|
|||||||
}
|
}
|
||||||
return acc;
|
return acc;
|
||||||
}, {} as GroupedResults);
|
}, {} as GroupedResults);
|
||||||
console.info(`Processed ${processedMessages.length} messages, results: ${JSON.stringify(groupedResults, undefined, 2)}`);
|
console.info(`Processed ${processedMessages.length} messages, results: ${JSON.stringify(groupedResults)}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,19 +71,19 @@ export default async function syncConnectedOnline() {
|
|||||||
return {
|
return {
|
||||||
id: service.ID,
|
id: service.ID,
|
||||||
clinic_id: service.ClinicID,
|
clinic_id: service.ClinicID,
|
||||||
code: service.Code,
|
sync_id: service.SyncID,
|
||||||
description: service.Description || null,
|
|
||||||
display: service.Display,
|
|
||||||
duration: service.Duration,
|
|
||||||
has_free_codes: !!service.HasFreeCodes,
|
|
||||||
name: service.Name,
|
name: service.Name,
|
||||||
|
description: service.Description || null,
|
||||||
|
price: service.Price,
|
||||||
|
requires_payment: !!service.RequiresPayment,
|
||||||
|
duration: service.Duration,
|
||||||
neto_duration: service.NetoDuration,
|
neto_duration: service.NetoDuration,
|
||||||
|
display: service.Display,
|
||||||
|
price_periods: service.PricePeriods || null,
|
||||||
online_hide_duration: service.OnlineHideDuration,
|
online_hide_duration: service.OnlineHideDuration,
|
||||||
online_hide_price: service.OnlineHidePrice,
|
online_hide_price: service.OnlineHidePrice,
|
||||||
price: service.Price,
|
code: service.Code,
|
||||||
price_periods: service.PricePeriods || null,
|
has_free_codes: !!service.HasFreeCodes,
|
||||||
requires_payment: !!service.RequiresPayment,
|
|
||||||
sync_id: service.SyncID,
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ export async function POST(request: NextRequest) {
|
|||||||
return NextResponse.json({}, { status: 401, statusText: 'Unauthorized' });
|
return NextResponse.json({}, { status: 401, statusText: 'Unauthorized' });
|
||||||
}
|
}
|
||||||
|
|
||||||
const analysisOrders = await getAnalysisOrdersAdmin({ orderStatus: 'QUEUED' });
|
const analysisOrders = await getAnalysisOrdersAdmin({ orderStatus: 'PROCESSING' });
|
||||||
|
|
||||||
console.error(`Sending test responses for ${analysisOrders.length} analysis orders`);
|
console.error(`Sending test responses for ${analysisOrders.length} analysis orders`);
|
||||||
for (const medreportOrder of analysisOrders) {
|
for (const medreportOrder of analysisOrders) {
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ export default function AnalysisView({
|
|||||||
feedback?: DoctorFeedback;
|
feedback?: DoctorFeedback;
|
||||||
}) {
|
}) {
|
||||||
const [isConfirmOpen, setIsConfirmOpen] = useState(false);
|
const [isConfirmOpen, setIsConfirmOpen] = useState(false);
|
||||||
|
const [isDraftSubmitting, setIsDraftSubmitting] = useState(false);
|
||||||
|
|
||||||
const { data: user } = useUser();
|
const { data: user } = useUser();
|
||||||
|
|
||||||
@@ -106,28 +107,22 @@ export default function AnalysisView({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleDraftSubmit = async (e: React.FormEvent) => {
|
const handleDraftSubmit = async (e: React.FormEvent) => {
|
||||||
|
setIsDraftSubmitting(true);
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
form.formState.errors.feedbackValue = undefined;
|
form.formState.errors.feedbackValue = undefined;
|
||||||
const formData = form.getValues();
|
const formData = form.getValues();
|
||||||
onSubmit(formData, 'DRAFT');
|
await onSubmit(formData, 'DRAFT');
|
||||||
|
setIsDraftSubmitting(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCompleteSubmit = async (e: React.FormEvent) => {
|
const handleCompleteSubmit = form.handleSubmit(async () => {
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
const isValid = await form.trigger();
|
|
||||||
if (!isValid) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsConfirmOpen(true);
|
setIsConfirmOpen(true);
|
||||||
};
|
});
|
||||||
|
|
||||||
const confirmComplete = () => {
|
const confirmComplete = form.handleSubmit(async (data) => {
|
||||||
const formData = form.getValues();
|
await onSubmit(data, 'COMPLETED');
|
||||||
onSubmit(formData, 'COMPLETED');
|
});
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -179,7 +174,11 @@ export default function AnalysisView({
|
|||||||
<div className="font-bold">
|
<div className="font-bold">
|
||||||
<Trans i18nKey="doctor:bmi" />
|
<Trans i18nKey="doctor:bmi" />
|
||||||
</div>
|
</div>
|
||||||
<div>{bmiFromMetric(patient?.weight ?? 0, patient?.height ?? 0)}</div>
|
<div>
|
||||||
|
{patient?.weight && patient?.height
|
||||||
|
? bmiFromMetric(patient.weight, patient.height)
|
||||||
|
: '-'}
|
||||||
|
</div>
|
||||||
<div className="font-bold">
|
<div className="font-bold">
|
||||||
<Trans i18nKey="doctor:smoking" />
|
<Trans i18nKey="doctor:smoking" />
|
||||||
</div>
|
</div>
|
||||||
@@ -245,7 +244,9 @@ export default function AnalysisView({
|
|||||||
type="button"
|
type="button"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={handleDraftSubmit}
|
onClick={handleDraftSubmit}
|
||||||
disabled={isReadOnly}
|
disabled={
|
||||||
|
isReadOnly || isDraftSubmitting || form.formState.isSubmitting
|
||||||
|
}
|
||||||
className="xs:w-1/4 w-full"
|
className="xs:w-1/4 w-full"
|
||||||
>
|
>
|
||||||
<Trans i18nKey="common:saveAsDraft" />
|
<Trans i18nKey="common:saveAsDraft" />
|
||||||
@@ -253,7 +254,9 @@ export default function AnalysisView({
|
|||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={handleCompleteSubmit}
|
onClick={handleCompleteSubmit}
|
||||||
disabled={isReadOnly}
|
disabled={
|
||||||
|
isReadOnly || isDraftSubmitting || form.formState.isSubmitting
|
||||||
|
}
|
||||||
className="xs:w-1/4 w-full"
|
className="xs:w-1/4 w-full"
|
||||||
>
|
>
|
||||||
<Trans i18nKey="common:save" />
|
<Trans i18nKey="common:save" />
|
||||||
|
|||||||
41
app/home/(user)/(dashboard)/booking/[handle]/page.tsx
Normal file
41
app/home/(user)/(dashboard)/booking/[handle]/page.tsx
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import { HomeLayoutPageHeader } from '@/app/home/(user)/_components/home-page-header';
|
||||||
|
import { loadCategory } from '@/app/home/(user)/_lib/server/load-category';
|
||||||
|
|
||||||
|
import { AppBreadcrumbs } from '@kit/ui/makerkit/app-breadcrumbs';
|
||||||
|
import { PageBody } from '@kit/ui/page';
|
||||||
|
import { Trans } from '@kit/ui/trans';
|
||||||
|
|
||||||
|
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
|
||||||
|
import { withI18n } from '~/lib/i18n/with-i18n';
|
||||||
|
|
||||||
|
export const generateMetadata = async () => {
|
||||||
|
const i18n = await createI18nServerInstance();
|
||||||
|
const title = i18n.t('booking:title');
|
||||||
|
|
||||||
|
return {
|
||||||
|
title,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
async function BookingHandlePage({ params }: { params: { handle: string } }) {
|
||||||
|
const handle = await params.handle;
|
||||||
|
const { category } = await loadCategory({ handle });
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<AppBreadcrumbs
|
||||||
|
values={{
|
||||||
|
[handle]: category?.name || handle,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<HomeLayoutPageHeader
|
||||||
|
title={<Trans i18nKey={'booking:title'} />}
|
||||||
|
description={<Trans i18nKey={'booking:description'} />}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<PageBody></PageBody>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withI18n(BookingHandlePage);
|
||||||
@@ -1,12 +1,16 @@
|
|||||||
|
import { use } from 'react';
|
||||||
|
|
||||||
|
import { AppBreadcrumbs } from '@kit/ui/makerkit/app-breadcrumbs';
|
||||||
import { PageBody } from '@kit/ui/page';
|
import { PageBody } from '@kit/ui/page';
|
||||||
import { Trans } from '@kit/ui/trans';
|
import { Trans } from '@kit/ui/trans';
|
||||||
|
|
||||||
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
|
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
|
||||||
import { withI18n } from '~/lib/i18n/with-i18n';
|
import { withI18n } from '~/lib/i18n/with-i18n';
|
||||||
|
|
||||||
|
|
||||||
import { HomeLayoutPageHeader } from '../../_components/home-page-header';
|
import { HomeLayoutPageHeader } from '../../_components/home-page-header';
|
||||||
import OrderCards from '../../_components/order-cards';
|
import OrderCards from '../../_components/order-cards';
|
||||||
|
import ServiceCategories from '../../_components/service-categories';
|
||||||
|
import { loadTtoServices } from '../../_lib/server/load-tto-services';
|
||||||
|
|
||||||
export const generateMetadata = async () => {
|
export const generateMetadata = async () => {
|
||||||
const i18n = await createI18nServerInstance();
|
const i18n = await createI18nServerInstance();
|
||||||
@@ -18,15 +22,30 @@ export const generateMetadata = async () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
function BookingPage() {
|
function BookingPage() {
|
||||||
|
const { heroCategories, ttoCategories } = use(loadTtoServices());
|
||||||
|
|
||||||
|
if (!heroCategories.length && !ttoCategories.length) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<AppBreadcrumbs />
|
||||||
|
<h3 className="mt-8">
|
||||||
|
<Trans i18nKey="booking:noCategories" />
|
||||||
|
</h3>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
<AppBreadcrumbs />
|
||||||
<HomeLayoutPageHeader
|
<HomeLayoutPageHeader
|
||||||
title={<Trans i18nKey={'booking:title'} />}
|
title={<Trans i18nKey={'booking:title'} />}
|
||||||
description={<Trans i18nKey={'booking:description'} />}
|
description={<Trans i18nKey={'booking:description'} />}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<PageBody>
|
<PageBody className="space-y-2">
|
||||||
<OrderCards />
|
<OrderCards heroCategories={heroCategories} />
|
||||||
|
<ServiceCategories categories={ttoCategories} />
|
||||||
</PageBody>
|
</PageBody>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,74 +1,68 @@
|
|||||||
"use client";
|
'use client';
|
||||||
|
|
||||||
import { ChevronRight, HeartPulse } from 'lucide-react';
|
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
import { pathsConfig } from '@/packages/shared/src/config';
|
||||||
|
import { ComponentInstanceIcon } from '@radix-ui/react-icons';
|
||||||
|
import { ChevronRight, HeartPulse } from 'lucide-react';
|
||||||
|
|
||||||
import { Button } from '@kit/ui/button';
|
import { Button } from '@kit/ui/button';
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
CardHeader,
|
|
||||||
CardDescription,
|
CardDescription,
|
||||||
CardProps,
|
|
||||||
CardFooter,
|
CardFooter,
|
||||||
|
CardHeader,
|
||||||
|
CardProps,
|
||||||
} from '@kit/ui/card';
|
} from '@kit/ui/card';
|
||||||
import { Trans } from '@kit/ui/trans';
|
|
||||||
import { cn } from '@/lib/utils';
|
|
||||||
|
|
||||||
const dummyCards = [
|
import { ServiceCategory } from './service-categories';
|
||||||
{
|
|
||||||
title: 'booking:analysisPackages.title',
|
|
||||||
description: 'booking:analysisPackages.description',
|
|
||||||
descriptionColor: 'text-primary',
|
|
||||||
icon: (
|
|
||||||
<Link href={'/home/order-analysis-package'}>
|
|
||||||
<Button size="icon" variant="outline" className="px-2 text-black">
|
|
||||||
<ChevronRight className="size-4 stroke-2" />
|
|
||||||
</Button>
|
|
||||||
</Link>
|
|
||||||
),
|
|
||||||
cardVariant: 'gradient-success' as CardProps['variant'],
|
|
||||||
iconBg: 'bg-warning',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export default function OrderCards() {
|
export default function OrderCards({
|
||||||
|
heroCategories,
|
||||||
|
}: {
|
||||||
|
heroCategories: ServiceCategory[];
|
||||||
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className="grid grid-cols-3 gap-6 mt-4">
|
<div className="xs:grid-cols-3 mt-4 grid grid-cols-1 gap-2">
|
||||||
{dummyCards.map(({
|
{heroCategories.map(({ name, description, color, handle }) => (
|
||||||
title,
|
|
||||||
description,
|
|
||||||
icon,
|
|
||||||
cardVariant,
|
|
||||||
descriptionColor,
|
|
||||||
iconBg,
|
|
||||||
}) => (
|
|
||||||
<Card
|
<Card
|
||||||
key={title}
|
key={name}
|
||||||
variant={cardVariant}
|
variant={`gradient-${color}` as CardProps['variant']}
|
||||||
className="flex flex-col justify-between"
|
className="flex flex-col justify-between"
|
||||||
>
|
>
|
||||||
<CardHeader className="items-end-safe">
|
<CardHeader className="relative flex flex-row justify-between">
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex size-8 items-center-safe justify-center-safe rounded-full text-white',
|
'flex size-8 items-center-safe justify-center-safe rounded-full',
|
||||||
iconBg,
|
`text-${color}`,
|
||||||
|
`bg-${color}/10`,
|
||||||
|
{
|
||||||
|
'bg-primary/10': color === 'success',
|
||||||
|
},
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{icon}
|
<ComponentInstanceIcon
|
||||||
|
className={cn('size-4', `fill-${color}`)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="absolute top-2 right-2 flex size-8 items-center-safe justify-center-safe rounded-xl text-white">
|
||||||
|
<Link
|
||||||
|
href={pathsConfig.app.bookingHandle.replace('[handle]', handle)}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
size="icon"
|
||||||
|
variant="outline"
|
||||||
|
className="px-2 text-black"
|
||||||
|
>
|
||||||
|
<ChevronRight className="size-4 stroke-2" />
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardFooter className="flex flex-col items-start gap-2">
|
<CardFooter className="mt-5 flex flex-col items-start gap-2">
|
||||||
<div
|
<h5>{name}</h5>
|
||||||
className={'flex size-8 items-center-safe justify-center-safe rounded-full text-white bg-primary\/10 mb-6'}
|
<CardDescription>{description}</CardDescription>
|
||||||
>
|
|
||||||
<HeartPulse className="size-4 fill-green-500" />
|
|
||||||
</div>
|
|
||||||
<h5>
|
|
||||||
<Trans i18nKey={title} />
|
|
||||||
</h5>
|
|
||||||
<CardDescription className={descriptionColor}>
|
|
||||||
<Trans i18nKey={description} />
|
|
||||||
</CardDescription>
|
|
||||||
</CardFooter>
|
</CardFooter>
|
||||||
</Card>
|
</Card>
|
||||||
))}
|
))}
|
||||||
|
|||||||
61
app/home/(user)/_components/service-categories.tsx
Normal file
61
app/home/(user)/_components/service-categories.tsx
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { redirect } from 'next/navigation';
|
||||||
|
|
||||||
|
import { createPath, pathsConfig } from '@/packages/shared/src/config';
|
||||||
|
import { ComponentInstanceIcon } from '@radix-ui/react-icons';
|
||||||
|
|
||||||
|
import { cn } from '@kit/ui/shadcn';
|
||||||
|
import { Card, CardDescription, CardTitle } from '@kit/ui/shadcn/card';
|
||||||
|
|
||||||
|
export interface ServiceCategory {
|
||||||
|
name: string;
|
||||||
|
handle: string;
|
||||||
|
color: string;
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ServiceCategories = ({
|
||||||
|
categories,
|
||||||
|
}: {
|
||||||
|
categories: ServiceCategory[];
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div className="grid grid-cols-1 gap-2 sm:grid-cols-2 md:grid-cols-3">
|
||||||
|
{categories.map((category, index) => (
|
||||||
|
<Card
|
||||||
|
key={index}
|
||||||
|
className="flex cursor-pointer gap-2 p-4 shadow hover:shadow-md"
|
||||||
|
onClick={() => {
|
||||||
|
redirect(
|
||||||
|
pathsConfig.app.bookingHandle.replace(
|
||||||
|
'[handle]',
|
||||||
|
category.handle,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'flex size-8 items-center-safe justify-center-safe rounded-full',
|
||||||
|
`bg-${category.color}/10`,
|
||||||
|
`text-${category.color}`,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<ComponentInstanceIcon />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h5 className="mb-2 text-lg font-semibold">{category.name}</h5>
|
||||||
|
<CardDescription className="">
|
||||||
|
{category.description}
|
||||||
|
</CardDescription>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ServiceCategories;
|
||||||
@@ -1,9 +1,11 @@
|
|||||||
import { cache } from 'react';
|
import { cache } from 'react';
|
||||||
|
|
||||||
import { listProductTypes } from "@lib/data/products";
|
|
||||||
import { listRegions } from '@lib/data/regions';
|
|
||||||
import { getProductCategories } from '@lib/data/categories';
|
import { getProductCategories } from '@lib/data/categories';
|
||||||
|
import { listProductTypes } from '@lib/data/products';
|
||||||
|
import { listRegions } from '@lib/data/regions';
|
||||||
|
|
||||||
import { OrderAnalysisCard } from '../../_components/order-analyses-cards';
|
import { OrderAnalysisCard } from '../../_components/order-analyses-cards';
|
||||||
|
import { ServiceCategory } from '../../_components/service-categories';
|
||||||
|
|
||||||
async function countryCodesLoader() {
|
async function countryCodesLoader() {
|
||||||
const countryCodes = await listRegions().then((regions) =>
|
const countryCodes = await listRegions().then((regions) =>
|
||||||
@@ -14,7 +16,9 @@ async function countryCodesLoader() {
|
|||||||
export const loadCountryCodes = cache(countryCodesLoader);
|
export const loadCountryCodes = cache(countryCodesLoader);
|
||||||
|
|
||||||
async function productCategoriesLoader() {
|
async function productCategoriesLoader() {
|
||||||
const productCategories = await getProductCategories({ fields: "*products, *products.variants" });
|
const productCategories = await getProductCategories({
|
||||||
|
fields: '*products, *products.variants, is_active',
|
||||||
|
});
|
||||||
return productCategories.product_categories ?? [];
|
return productCategories.product_categories ?? [];
|
||||||
}
|
}
|
||||||
export const loadProductCategories = cache(productCategoriesLoader);
|
export const loadProductCategories = cache(productCategoriesLoader);
|
||||||
@@ -32,22 +36,31 @@ async function analysesLoader() {
|
|||||||
]);
|
]);
|
||||||
const countryCode = countryCodes[0]!;
|
const countryCode = countryCodes[0]!;
|
||||||
|
|
||||||
const category = productCategories.find(({ metadata }) => metadata?.page === 'order-analysis');
|
const category = productCategories.find(
|
||||||
|
({ metadata }) => metadata?.page === 'order-analysis',
|
||||||
|
);
|
||||||
|
const serviceCategories = productCategories.filter(
|
||||||
|
({ parent_category }) => parent_category?.handle === 'tto-categories',
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
analyses: category?.products?.map<OrderAnalysisCard>(({ title, description, subtitle, variants, status, metadata }) => {
|
analyses:
|
||||||
const variant = variants![0]!;
|
category?.products?.map<OrderAnalysisCard>(
|
||||||
return {
|
({ title, description, subtitle, variants, status, metadata }) => {
|
||||||
title,
|
const variant = variants![0]!;
|
||||||
description,
|
return {
|
||||||
subtitle,
|
title,
|
||||||
variant: {
|
description,
|
||||||
id: variant.id,
|
subtitle,
|
||||||
|
variant: {
|
||||||
|
id: variant.id,
|
||||||
|
},
|
||||||
|
isAvailable:
|
||||||
|
status === 'published' && !!metadata?.analysisIdOriginal,
|
||||||
|
};
|
||||||
},
|
},
|
||||||
isAvailable: status === 'published' && !!metadata?.analysisIdOriginal,
|
) ?? [],
|
||||||
};
|
|
||||||
}) ?? [],
|
|
||||||
countryCode,
|
countryCode,
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
export const loadAnalyses = cache(analysesLoader);
|
export const loadAnalyses = cache(analysesLoader);
|
||||||
|
|||||||
31
app/home/(user)/_lib/server/load-category.ts
Normal file
31
app/home/(user)/_lib/server/load-category.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import { cache } from 'react';
|
||||||
|
|
||||||
|
import { getProductCategories } from '@lib/data';
|
||||||
|
|
||||||
|
import { ServiceCategory } from '../../_components/service-categories';
|
||||||
|
|
||||||
|
async function categoryLoader({
|
||||||
|
handle,
|
||||||
|
}: {
|
||||||
|
handle: string;
|
||||||
|
}): Promise<{ category: ServiceCategory | null }> {
|
||||||
|
const response = await getProductCategories({
|
||||||
|
handle,
|
||||||
|
fields: '*products, is_active, metadata',
|
||||||
|
});
|
||||||
|
|
||||||
|
const category = response.product_categories[0];
|
||||||
|
|
||||||
|
return {
|
||||||
|
category: {
|
||||||
|
color:
|
||||||
|
typeof category?.metadata?.color === 'string'
|
||||||
|
? category?.metadata?.color
|
||||||
|
: 'primary',
|
||||||
|
description: category?.description || '',
|
||||||
|
handle: category?.handle || '',
|
||||||
|
name: category?.name || '',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
export const loadCategory = cache(categoryLoader);
|
||||||
49
app/home/(user)/_lib/server/load-tto-services.ts
Normal file
49
app/home/(user)/_lib/server/load-tto-services.ts
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import { cache } from 'react';
|
||||||
|
|
||||||
|
import { getProductCategories } from '@lib/data';
|
||||||
|
|
||||||
|
import { ServiceCategory } from '../../_components/service-categories';
|
||||||
|
|
||||||
|
async function ttoServicesLoader() {
|
||||||
|
const response = await getProductCategories({
|
||||||
|
fields: '*products, is_active, metadata',
|
||||||
|
});
|
||||||
|
|
||||||
|
const heroCategories = response.product_categories?.filter(
|
||||||
|
({ parent_category, is_active, metadata }) =>
|
||||||
|
parent_category?.handle === 'tto-categories' &&
|
||||||
|
is_active &&
|
||||||
|
metadata?.isHero,
|
||||||
|
);
|
||||||
|
|
||||||
|
const ttoCategories = response.product_categories?.filter(
|
||||||
|
({ parent_category, is_active, metadata }) =>
|
||||||
|
parent_category?.handle === 'tto-categories' &&
|
||||||
|
is_active &&
|
||||||
|
!metadata?.isHero,
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
heroCategories:
|
||||||
|
heroCategories.map<ServiceCategory>(
|
||||||
|
({ name, handle, metadata, description }) => ({
|
||||||
|
name,
|
||||||
|
handle,
|
||||||
|
color:
|
||||||
|
typeof metadata?.color === 'string' ? metadata.color : 'primary',
|
||||||
|
description,
|
||||||
|
}),
|
||||||
|
) ?? [],
|
||||||
|
ttoCategories:
|
||||||
|
ttoCategories.map<ServiceCategory>(
|
||||||
|
({ name, handle, metadata, description }) => ({
|
||||||
|
name,
|
||||||
|
handle,
|
||||||
|
color:
|
||||||
|
typeof metadata?.color === 'string' ? metadata.color : 'primary',
|
||||||
|
description,
|
||||||
|
}),
|
||||||
|
) ?? [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
export const loadTtoServices = cache(ttoServicesLoader);
|
||||||
35
lib/services/audit/notificationEntries.service.ts
Normal file
35
lib/services/audit/notificationEntries.service.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import { Database } from '@kit/supabase/database';
|
||||||
|
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
||||||
|
|
||||||
|
export enum NotificationAction {
|
||||||
|
DOCTOR_FEEDBACK_RECEIVED = 'DOCTOR_FEEDBACK_RECEIVED',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createNotificationLog = async ({
|
||||||
|
action,
|
||||||
|
status,
|
||||||
|
comment,
|
||||||
|
relatedRecordId,
|
||||||
|
}: {
|
||||||
|
action: NotificationAction;
|
||||||
|
status: Database['audit']['Enums']['action_status'];
|
||||||
|
comment?: string;
|
||||||
|
relatedRecordId?: string | number;
|
||||||
|
}) => {
|
||||||
|
try {
|
||||||
|
const supabase = getSupabaseServerClient();
|
||||||
|
|
||||||
|
await supabase
|
||||||
|
.schema('audit')
|
||||||
|
.from('notification_entries')
|
||||||
|
.insert({
|
||||||
|
action,
|
||||||
|
status,
|
||||||
|
comment,
|
||||||
|
related_record_key: relatedRecordId?.toString(),
|
||||||
|
})
|
||||||
|
.throwOnError();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to insert doctor page view log', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -1,10 +1,34 @@
|
|||||||
'use server';
|
'use server';
|
||||||
|
|
||||||
|
import { CompanySubmitData } from '@/lib/types/company';
|
||||||
|
import { emailSchema } from '@/lib/validations/email.schema';
|
||||||
|
|
||||||
|
import { renderDoctorSummaryReceivedEmail } from '@kit/email-templates';
|
||||||
import { getMailer } from '@kit/mailers';
|
import { getMailer } from '@kit/mailers';
|
||||||
import { enhanceAction } from '@kit/next/actions';
|
import { enhanceAction } from '@kit/next/actions';
|
||||||
|
import { getLogger } from '@kit/shared/logger';
|
||||||
|
|
||||||
import { CompanySubmitData } from '../types/company';
|
export const sendDoctorSummaryCompletedEmail = async (
|
||||||
import { emailSchema } from '../validations/email.schema';
|
language: string,
|
||||||
|
recipientName: string,
|
||||||
|
recipientEmail: string,
|
||||||
|
orderNr: string,
|
||||||
|
orderId: number,
|
||||||
|
) => {
|
||||||
|
const { html, subject } = await renderDoctorSummaryReceivedEmail({
|
||||||
|
language,
|
||||||
|
recipientName,
|
||||||
|
recipientEmail,
|
||||||
|
orderNr,
|
||||||
|
orderId,
|
||||||
|
});
|
||||||
|
|
||||||
|
await sendEmail({
|
||||||
|
subject,
|
||||||
|
html,
|
||||||
|
to: recipientEmail,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
export const sendCompanyOfferEmail = async (
|
export const sendCompanyOfferEmail = async (
|
||||||
data: CompanySubmitData,
|
data: CompanySubmitData,
|
||||||
@@ -26,13 +50,24 @@ export const sendCompanyOfferEmail = async (
|
|||||||
export const sendEmail = enhanceAction(
|
export const sendEmail = enhanceAction(
|
||||||
async ({ subject, html, to }) => {
|
async ({ subject, html, to }) => {
|
||||||
const mailer = await getMailer();
|
const mailer = await getMailer();
|
||||||
|
const log = await getLogger();
|
||||||
|
|
||||||
await mailer.sendEmail({
|
if (!process.env.EMAIL_USER) {
|
||||||
|
log.error('Sending email failed, as no sender found in env.')
|
||||||
|
throw new Error('No email user configured');
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await mailer.sendEmail({
|
||||||
|
from: process.env.EMAIL_USER,
|
||||||
to,
|
to,
|
||||||
subject,
|
subject,
|
||||||
html,
|
html,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
log.info(
|
||||||
|
`Sent email with subject "${subject}", result: ${JSON.stringify(result)}`,
|
||||||
|
);
|
||||||
|
|
||||||
return {};
|
return {};
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,42 +1,45 @@
|
|||||||
'use server';
|
'use server';
|
||||||
|
|
||||||
import { z } from 'zod';
|
|
||||||
import { loadCurrentUserAccount } from '@/app/home/(user)/_lib/server/load-user-account';
|
import { loadCurrentUserAccount } from '@/app/home/(user)/_lib/server/load-user-account';
|
||||||
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
import { MontonioOrderHandlerService } from '@/packages/billing/montonio/src';
|
||||||
import { addToCart, deleteLineItem, retrieveCart } from '@lib/data/cart';
|
import { addToCart, deleteLineItem, retrieveCart } from '@lib/data/cart';
|
||||||
import { getCartId } from '@lib/data/cookies';
|
import { getCartId } from '@lib/data/cookies';
|
||||||
import { StoreCartLineItem, StoreProductVariant } from '@medusajs/types';
|
import { StoreCartLineItem, StoreProductVariant } from '@medusajs/types';
|
||||||
import { MontonioOrderHandlerService } from '@/packages/billing/montonio/src';
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
||||||
|
|
||||||
import { requireUserInServerComponent } from '../server/require-user-in-server-component';
|
import { requireUserInServerComponent } from '../server/require-user-in-server-component';
|
||||||
|
|
||||||
const env = () => z
|
const env = () =>
|
||||||
.object({
|
z
|
||||||
medusaBackendPublicUrl: z
|
.object({
|
||||||
.string({
|
medusaBackendPublicUrl: z
|
||||||
required_error: 'MEDUSA_BACKEND_PUBLIC_URL is required',
|
.string({
|
||||||
})
|
required_error: 'MEDUSA_BACKEND_PUBLIC_URL is required',
|
||||||
.min(1),
|
})
|
||||||
siteUrl: z
|
.min(1),
|
||||||
.string({
|
siteUrl: z
|
||||||
required_error: 'NEXT_PUBLIC_SITE_URL is required',
|
.string({
|
||||||
})
|
required_error: 'NEXT_PUBLIC_SITE_URL is required',
|
||||||
.min(1),
|
})
|
||||||
})
|
.min(1),
|
||||||
.parse({
|
})
|
||||||
medusaBackendPublicUrl: process.env.MEDUSA_BACKEND_PUBLIC_URL!,
|
.parse({
|
||||||
siteUrl: process.env.NEXT_PUBLIC_SITE_URL!,
|
medusaBackendPublicUrl: process.env.MEDUSA_BACKEND_PUBLIC_URL!,
|
||||||
});
|
siteUrl: process.env.NEXT_PUBLIC_SITE_URL!,
|
||||||
|
});
|
||||||
|
|
||||||
export async function handleAddToCart({
|
export async function handleAddToCart({
|
||||||
selectedVariant,
|
selectedVariant,
|
||||||
countryCode,
|
countryCode,
|
||||||
}: {
|
}: {
|
||||||
selectedVariant: Pick<StoreProductVariant, 'id'>
|
selectedVariant: Pick<StoreProductVariant, 'id'>;
|
||||||
countryCode: string
|
countryCode: string;
|
||||||
}) {
|
}) {
|
||||||
const supabase = getSupabaseServerClient();
|
const supabase = getSupabaseServerClient();
|
||||||
const user = await requireUserInServerComponent();
|
const user = await requireUserInServerComponent();
|
||||||
const account = await loadCurrentUserAccount()
|
const account = await loadCurrentUserAccount();
|
||||||
if (!account) {
|
if (!account) {
|
||||||
throw new Error('Account not found');
|
throw new Error('Account not found');
|
||||||
}
|
}
|
||||||
@@ -48,16 +51,13 @@ export async function handleAddToCart({
|
|||||||
countryCode,
|
countryCode,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { error } = await supabase
|
const { error } = await supabase.schema('audit').from('cart_entries').insert({
|
||||||
.schema('audit')
|
variant_id: selectedVariant.id,
|
||||||
.from('cart_entries')
|
operation: 'ADD_TO_CART',
|
||||||
.insert({
|
account_id: account.id,
|
||||||
variant_id: selectedVariant.id,
|
cart_id: cart.id,
|
||||||
operation: 'ADD_TO_CART',
|
changed_by: user.id,
|
||||||
account_id: account.id,
|
});
|
||||||
cart_id: cart.id,
|
|
||||||
changed_by: user.id,
|
|
||||||
});
|
|
||||||
if (error) {
|
if (error) {
|
||||||
throw new Error('Error logging cart entry: ' + error.message);
|
throw new Error('Error logging cart entry: ' + error.message);
|
||||||
}
|
}
|
||||||
@@ -65,68 +65,65 @@ export async function handleAddToCart({
|
|||||||
return cart;
|
return cart;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function handleDeleteCartItem({
|
export async function handleDeleteCartItem({ lineId }: { lineId: string }) {
|
||||||
lineId,
|
|
||||||
}: {
|
|
||||||
lineId: string;
|
|
||||||
}) {
|
|
||||||
await deleteLineItem(lineId);
|
await deleteLineItem(lineId);
|
||||||
|
|
||||||
const supabase = getSupabaseServerClient();
|
const supabase = getSupabaseServerClient();
|
||||||
const cartId = await getCartId();
|
const cartId = await getCartId();
|
||||||
const user = await requireUserInServerComponent();
|
const user = await requireUserInServerComponent();
|
||||||
const account = await loadCurrentUserAccount()
|
const account = await loadCurrentUserAccount();
|
||||||
if (!account) {
|
if (!account) {
|
||||||
throw new Error('Account not found');
|
throw new Error('Account not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
const { error } = await supabase
|
const { error } = await supabase.schema('audit').from('cart_entries').insert({
|
||||||
.schema('audit')
|
variant_id: lineId,
|
||||||
.from('cart_entries')
|
operation: 'REMOVE_FROM_CART',
|
||||||
.insert({
|
account_id: account.id,
|
||||||
variant_id: lineId,
|
cart_id: cartId!,
|
||||||
operation: 'REMOVE_FROM_CART',
|
changed_by: user.id,
|
||||||
account_id: account.id,
|
});
|
||||||
cart_id: cartId!,
|
|
||||||
changed_by: user.id,
|
|
||||||
});
|
|
||||||
if (error) {
|
if (error) {
|
||||||
throw new Error('Error logging cart entry: ' + error.message);
|
throw new Error('Error logging cart entry: ' + error.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function handleNavigateToPayment({ language, paymentSessionId }: { language: string, paymentSessionId: string }) {
|
export async function handleNavigateToPayment({
|
||||||
|
language,
|
||||||
|
paymentSessionId,
|
||||||
|
}: {
|
||||||
|
language: string;
|
||||||
|
paymentSessionId: string;
|
||||||
|
}) {
|
||||||
const supabase = getSupabaseServerClient();
|
const supabase = getSupabaseServerClient();
|
||||||
const user = await requireUserInServerComponent();
|
const user = await requireUserInServerComponent();
|
||||||
const account = await loadCurrentUserAccount()
|
const account = await loadCurrentUserAccount();
|
||||||
if (!account) {
|
if (!account) {
|
||||||
throw new Error('Account not found');
|
throw new Error('Account not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
const cart = await retrieveCart();
|
const cart = await retrieveCart();
|
||||||
if (!cart) {
|
if (!cart) {
|
||||||
throw new Error("No cart found");
|
throw new Error('No cart found');
|
||||||
}
|
}
|
||||||
|
|
||||||
const paymentLink = await new MontonioOrderHandlerService().getMontonioPaymentLink({
|
const paymentLink =
|
||||||
notificationUrl: `${env().medusaBackendPublicUrl}/hooks/payment/montonio_montonio`,
|
await new MontonioOrderHandlerService().getMontonioPaymentLink({
|
||||||
returnUrl: `${env().siteUrl}/home/cart/montonio-callback`,
|
notificationUrl: `${env().medusaBackendPublicUrl}/hooks/payment/montonio_montonio`,
|
||||||
amount: cart.total,
|
returnUrl: `${env().siteUrl}/home/cart/montonio-callback`,
|
||||||
currency: cart.currency_code.toUpperCase(),
|
amount: cart.total,
|
||||||
description: `Order from Medreport`,
|
currency: cart.currency_code.toUpperCase(),
|
||||||
locale: language,
|
description: `Order from Medreport`,
|
||||||
merchantReference: `${account.id}:${paymentSessionId}:${cart.id}`,
|
locale: language,
|
||||||
});
|
merchantReference: `${account.id}:${paymentSessionId}:${cart.id}`,
|
||||||
|
|
||||||
const { error } = await supabase
|
|
||||||
.schema('audit')
|
|
||||||
.from('cart_entries')
|
|
||||||
.insert({
|
|
||||||
operation: 'NAVIGATE_TO_PAYMENT',
|
|
||||||
account_id: account.id,
|
|
||||||
cart_id: cart.id,
|
|
||||||
changed_by: user.id,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { error } = await supabase.schema('audit').from('cart_entries').insert({
|
||||||
|
operation: 'NAVIGATE_TO_PAYMENT',
|
||||||
|
account_id: account.id,
|
||||||
|
cart_id: cart.id,
|
||||||
|
changed_by: user.id,
|
||||||
|
});
|
||||||
if (error) {
|
if (error) {
|
||||||
throw new Error('Error logging cart entry: ' + error.message);
|
throw new Error('Error logging cart entry: ' + error.message);
|
||||||
}
|
}
|
||||||
@@ -137,26 +134,23 @@ export async function handleNavigateToPayment({ language, paymentSessionId }: {
|
|||||||
export async function handleLineItemTimeout({
|
export async function handleLineItemTimeout({
|
||||||
lineItem,
|
lineItem,
|
||||||
}: {
|
}: {
|
||||||
lineItem: StoreCartLineItem
|
lineItem: StoreCartLineItem;
|
||||||
}) {
|
}) {
|
||||||
const supabase = getSupabaseServerClient();
|
const supabase = getSupabaseServerClient();
|
||||||
const user = await requireUserInServerComponent();
|
const user = await requireUserInServerComponent();
|
||||||
const account = await loadCurrentUserAccount()
|
const account = await loadCurrentUserAccount();
|
||||||
if (!account) {
|
if (!account) {
|
||||||
throw new Error('Account not found');
|
throw new Error('Account not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
await deleteLineItem(lineItem.id);
|
await deleteLineItem(lineItem.id);
|
||||||
|
|
||||||
const { error } = await supabase
|
const { error } = await supabase.schema('audit').from('cart_entries').insert({
|
||||||
.schema('audit')
|
operation: 'LINE_ITEM_TIMEOUT',
|
||||||
.from('cart_entries')
|
account_id: account.id,
|
||||||
.insert({
|
cart_id: lineItem.cart_id,
|
||||||
operation: 'LINE_ITEM_TIMEOUT',
|
changed_by: user.id,
|
||||||
account_id: account.id,
|
});
|
||||||
cart_id: lineItem.cart_id,
|
|
||||||
changed_by: user.id,
|
|
||||||
});
|
|
||||||
if (error) {
|
if (error) {
|
||||||
throw new Error('Error logging cart entry: ' + error.message);
|
throw new Error('Error logging cart entry: ' + error.message);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,5 +3,5 @@ import { z } from 'zod';
|
|||||||
export const emailSchema = z.object({
|
export const emailSchema = z.object({
|
||||||
to: z.string().email(),
|
to: z.string().email(),
|
||||||
subject: z.string().min(1).max(200),
|
subject: z.string().min(1).max(200),
|
||||||
html: z.string().min(1).max(5000),
|
html: z.string().min(1),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,97 @@
|
|||||||
|
import {
|
||||||
|
Body,
|
||||||
|
Button,
|
||||||
|
Head,
|
||||||
|
Html,
|
||||||
|
Link,
|
||||||
|
Preview,
|
||||||
|
Tailwind,
|
||||||
|
Text,
|
||||||
|
render,
|
||||||
|
} from '@react-email/components';
|
||||||
|
|
||||||
|
import { BodyStyle } from '../components/body-style';
|
||||||
|
import CommonFooter from '../components/common-footer';
|
||||||
|
import { EmailContent } from '../components/content';
|
||||||
|
import { EmailHeader } from '../components/header';
|
||||||
|
import { EmailHeading } from '../components/heading';
|
||||||
|
import { EmailWrapper } from '../components/wrapper';
|
||||||
|
import { initializeEmailI18n } from '../lib/i18n';
|
||||||
|
|
||||||
|
export async function renderDoctorSummaryReceivedEmail({
|
||||||
|
language,
|
||||||
|
recipientEmail,
|
||||||
|
recipientName,
|
||||||
|
orderNr,
|
||||||
|
orderId,
|
||||||
|
}: {
|
||||||
|
language?: string;
|
||||||
|
recipientName: string;
|
||||||
|
recipientEmail: string;
|
||||||
|
orderNr: string;
|
||||||
|
orderId: number;
|
||||||
|
}) {
|
||||||
|
const namespace = 'doctor-summary-received-email';
|
||||||
|
|
||||||
|
const { t } = await initializeEmailI18n({
|
||||||
|
language,
|
||||||
|
namespace: [namespace, 'common'],
|
||||||
|
});
|
||||||
|
|
||||||
|
const to = recipientEmail;
|
||||||
|
|
||||||
|
const previewText = t(`${namespace}:previewText`, {
|
||||||
|
orderNr,
|
||||||
|
});
|
||||||
|
|
||||||
|
const subject = t(`${namespace}:subject`, {
|
||||||
|
orderNr,
|
||||||
|
});
|
||||||
|
|
||||||
|
const html = await render(
|
||||||
|
<Html>
|
||||||
|
<Head>
|
||||||
|
<BodyStyle />
|
||||||
|
</Head>
|
||||||
|
|
||||||
|
<Preview>{previewText}</Preview>
|
||||||
|
|
||||||
|
<Tailwind>
|
||||||
|
<Body>
|
||||||
|
<EmailWrapper>
|
||||||
|
<EmailHeader>
|
||||||
|
<EmailHeading>{previewText}</EmailHeading>
|
||||||
|
</EmailHeader>
|
||||||
|
|
||||||
|
<EmailContent>
|
||||||
|
<Text className="text-[16px] leading-[24px] text-[#242424]">
|
||||||
|
{t(`${namespace}:hello`, {
|
||||||
|
displayName: recipientName,
|
||||||
|
})}
|
||||||
|
</Text>
|
||||||
|
<Text className="text-[16px] leading-[24px] text-[#242424]">
|
||||||
|
{t(`${namespace}:summaryReceivedForOrder`, { orderNr })}
|
||||||
|
</Text>
|
||||||
|
<Link
|
||||||
|
href={`${process.env.NEXT_PUBLIC_SITE_URL}/home/order/${orderId}`}
|
||||||
|
>
|
||||||
|
<Button> {t(`${namespace}:linkText`, { orderNr })}</Button>
|
||||||
|
</Link>
|
||||||
|
<Text>
|
||||||
|
{t(`${namespace}:ifButtonDisabled`)}{' '}
|
||||||
|
{`${process.env.NEXT_PUBLIC_SITE_URL}/home/order/${orderId}`}
|
||||||
|
</Text>
|
||||||
|
<CommonFooter t={t} />
|
||||||
|
</EmailContent>
|
||||||
|
</EmailWrapper>
|
||||||
|
</Body>
|
||||||
|
</Tailwind>
|
||||||
|
</Html>,
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
html,
|
||||||
|
subject,
|
||||||
|
to,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -3,3 +3,4 @@ export * from './emails/account-delete.email';
|
|||||||
export * from './emails/otp.email';
|
export * from './emails/otp.email';
|
||||||
export * from './emails/company-offer.email';
|
export * from './emails/company-offer.email';
|
||||||
export * from './emails/synlab.email';
|
export * from './emails/synlab.email';
|
||||||
|
export * from './emails/doctor-summary-received.email';
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"subject": "Doctor feedback to order {{orderNr}} received",
|
||||||
|
"previewText": "A doctor has submitted feedback on your analysis results.",
|
||||||
|
"hello": "Hello {{displayName}},",
|
||||||
|
"summaryReceivedForOrder": "A doctor has submitted feedback to your analysis results from order {{orderNr}}.",
|
||||||
|
"linkText": "View summary",
|
||||||
|
"ifButtonDisabled": "If clicking the button does not work, copy this link to your browser's url field:"
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"subject": "Saabus arsti kokkuvõtte tellimusele {{orderNr}}",
|
||||||
|
"previewText": "Arst on saatnud kokkuvõtte sinu analüüsitulemustele.",
|
||||||
|
"hello": "Tere, {{displayName}}",
|
||||||
|
"summaryReceivedForOrder": "Arst on koostanud selgitava kokkuvõtte sinu tellitud analüüsidele.",
|
||||||
|
"linkText": "Vaata kokkuvõtet",
|
||||||
|
"ifButtonDisabled": "Kui nupule vajutamine ei toimi, kopeeri see link oma brauserisse:"
|
||||||
|
}
|
||||||
8
packages/email-templates/src/locales/ru/common.json
Normal file
8
packages/email-templates/src/locales/ru/common.json
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"footer": {
|
||||||
|
"lines1": "MedReport",
|
||||||
|
"lines2": "E-mail: <a href=\"mailto:info@medreport.ee\">info@medreport.ee</a>",
|
||||||
|
"lines3": "Klienditugi: <a href=\"tel:+37258871517\">+372 5887 1517</a>",
|
||||||
|
"lines4": "<a href=\"https://www.medreport.ee\">www.medreport.ee</a>"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"subject": "Uus ettevõtte liitumispäring",
|
||||||
|
"previewText": "Ettevõte {{companyName}} soovib pakkumist",
|
||||||
|
"companyName": "Ettevõtte nimi:",
|
||||||
|
"contactPerson": "Kontaktisik:",
|
||||||
|
"email": "E-mail:",
|
||||||
|
"phone": "Telefon:"
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"subject": "Saabus arsti kokkuvõtte tellimusele {{orderNr}}",
|
||||||
|
"previewText": "Arst on saatnud kokkuvõtte sinu analüüsitulemustele.",
|
||||||
|
"hello": "Tere, {{displayName}}",
|
||||||
|
"summaryReceivedForOrder": "Arst on koostanud selgitava kokkuvõtte sinu tellitud analüüsidele.",
|
||||||
|
"linkText": "Vaata kokkuvõtet",
|
||||||
|
"ifButtonDisabled": "Kui nupule vajutamine ei toimi, kopeeri see link oma brauserisse:"
|
||||||
|
}
|
||||||
12
packages/email-templates/src/locales/ru/synlab-email.json
Normal file
12
packages/email-templates/src/locales/ru/synlab-email.json
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"subject": "Teie Medreport tellimus on kinnitatud - {{analysisPackageName}}",
|
||||||
|
"previewText": "Teie Medreport tellimus on kinnitatud - {{analysisPackageName}}",
|
||||||
|
"heading": "Teie Medreport tellimus on kinnitatud - {{analysisPackageName}}",
|
||||||
|
"hello": "Tere {{personName}},",
|
||||||
|
"lines1": "Saatekiri {{analysisPackageName}} analüüsi uuringuteks on saadetud laborisse digitaalselt. Palun mine proove andma: Synlab - {{partnerLocationName}}",
|
||||||
|
"lines2": "<i>Kui Teil ei ole võimalik valitud asukohta minna proove andma, siis võite minna endale sobivasse proovivõtupunkti - <a href=\"https://medreport.ee/et/verevotupunktid\">vaata asukohti ja lahtiolekuaegasid</a>.</i>",
|
||||||
|
"lines3": "Soovituslik on proove anda pigem hommikul (enne 12:00) ning söömata ja joomata (vett võib juua).",
|
||||||
|
"lines4": "Proovivõtupunktis valige järjekorrasüsteemis: <strong>saatekirjad</strong> alt <strong>eriarsti saatekiri</strong>.",
|
||||||
|
"lines5": "Juhul kui tekkis lisaküsimusi, siis võtke julgelt ühendust.",
|
||||||
|
"lines6": "SYNLAB klienditoe telefon: <a href=\"tel:+37217123\">17123</a>"
|
||||||
|
}
|
||||||
@@ -5,6 +5,10 @@ import { revalidatePath } from 'next/cache';
|
|||||||
import { enhanceAction } from '@kit/next/actions';
|
import { enhanceAction } from '@kit/next/actions';
|
||||||
import { getLogger } from '@kit/shared/logger';
|
import { getLogger } from '@kit/shared/logger';
|
||||||
|
|
||||||
|
import {
|
||||||
|
NotificationAction,
|
||||||
|
createNotificationLog,
|
||||||
|
} from '../../../../../../../lib/services/audit/notificationEntries.service';
|
||||||
import {
|
import {
|
||||||
DoctorAnalysisFeedbackTable,
|
DoctorAnalysisFeedbackTable,
|
||||||
DoctorJobSelect,
|
DoctorJobSelect,
|
||||||
@@ -107,6 +111,7 @@ export const giveFeedbackAction = doctorAction(
|
|||||||
status: DoctorAnalysisFeedbackTable['status'];
|
status: DoctorAnalysisFeedbackTable['status'];
|
||||||
}) => {
|
}) => {
|
||||||
const logger = await getLogger();
|
const logger = await getLogger();
|
||||||
|
const isCompleted = status === 'COMPLETED';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
logger.info(
|
logger.info(
|
||||||
@@ -118,8 +123,25 @@ export const giveFeedbackAction = doctorAction(
|
|||||||
logger.info({ analysisOrderId }, `Successfully submitted feedback`);
|
logger.info({ analysisOrderId }, `Successfully submitted feedback`);
|
||||||
|
|
||||||
revalidateDoctorAnalysis();
|
revalidateDoctorAnalysis();
|
||||||
|
|
||||||
|
if (isCompleted) {
|
||||||
|
await createNotificationLog({
|
||||||
|
action: NotificationAction.DOCTOR_FEEDBACK_RECEIVED,
|
||||||
|
status: 'SUCCESS',
|
||||||
|
relatedRecordId: analysisOrderId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return { success: true };
|
return { success: true };
|
||||||
} catch (e) {
|
} catch (e: any) {
|
||||||
|
if (isCompleted) {
|
||||||
|
await createNotificationLog({
|
||||||
|
action: NotificationAction.DOCTOR_FEEDBACK_RECEIVED,
|
||||||
|
status: 'FAIL',
|
||||||
|
comment: e?.message,
|
||||||
|
relatedRecordId: analysisOrderId,
|
||||||
|
});
|
||||||
|
}
|
||||||
logger.error('Failed to give feedback', e);
|
logger.error('Failed to give feedback', e);
|
||||||
return { success: false, reason: ErrorReason.UNKNOWN };
|
return { success: false, reason: ErrorReason.UNKNOWN };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,8 +2,10 @@ import 'server-only';
|
|||||||
|
|
||||||
import { isBefore } from 'date-fns';
|
import { isBefore } from 'date-fns';
|
||||||
|
|
||||||
|
import { getFullName } from '@kit/shared/utils';
|
||||||
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
||||||
|
|
||||||
|
import { sendDoctorSummaryCompletedEmail } from '../../../../../../../lib/services/mailer.service';
|
||||||
import { AnalysisResultDetails } from '../schema/doctor-analysis-detail-view.schema';
|
import { AnalysisResultDetails } from '../schema/doctor-analysis-detail-view.schema';
|
||||||
import {
|
import {
|
||||||
AnalysisResponseBase,
|
AnalysisResponseBase,
|
||||||
@@ -635,5 +637,42 @@ export async function submitFeedback(
|
|||||||
throw new Error('Something went wrong');
|
throw new Error('Something went wrong');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (status === 'COMPLETED') {
|
||||||
|
const [{ data: recipient }, { data: medusaOrderIds }] = await Promise.all([
|
||||||
|
supabase
|
||||||
|
.schema('medreport')
|
||||||
|
.from('accounts')
|
||||||
|
.select('name, last_name, email, preferred_locale')
|
||||||
|
.eq('is_personal_account', true)
|
||||||
|
.eq('primary_owner_user_id', userId)
|
||||||
|
.throwOnError(),
|
||||||
|
supabase
|
||||||
|
.schema('medreport')
|
||||||
|
.from('analysis_orders')
|
||||||
|
.select('medusa_order_id, id')
|
||||||
|
.eq('id', analysisOrderId)
|
||||||
|
.limit(1)
|
||||||
|
.throwOnError(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!recipient?.[0]?.email) {
|
||||||
|
throw new Error('Could not find user email.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!medusaOrderIds?.[0]?.id) {
|
||||||
|
throw new Error('Could not retrieve order.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const { preferred_locale, name, last_name, email } = recipient[0];
|
||||||
|
|
||||||
|
await sendDoctorSummaryCompletedEmail(
|
||||||
|
preferred_locale ?? 'et',
|
||||||
|
getFullName(name, last_name),
|
||||||
|
email,
|
||||||
|
medusaOrderIds?.[0]?.medusa_order_id ?? '',
|
||||||
|
medusaOrderIds[0].id,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import { sdk } from "@lib/config"
|
import { sdk } from "@lib/config";
|
||||||
import { HttpTypes } from "@medusajs/types"
|
import { HttpTypes } from "@medusajs/types";
|
||||||
import { getCacheOptions } from "./cookies"
|
import { getCacheOptions } from "./cookies";
|
||||||
|
|
||||||
export const listCategories = async (query?: Record<string, any>) => {
|
export const listCategories = async (query?: Record<string, any>) => {
|
||||||
const next = {
|
const next = {
|
||||||
...(await getCacheOptions("categories")),
|
...(await getCacheOptions("categories")),
|
||||||
}
|
};
|
||||||
|
|
||||||
const limit = query?.limit || 100
|
const limit = query?.limit || 100;
|
||||||
|
|
||||||
return sdk.client
|
return sdk.client
|
||||||
.fetch<{ product_categories: HttpTypes.StoreProductCategory[] }>(
|
.fetch<{ product_categories: HttpTypes.StoreProductCategory[] }>(
|
||||||
@@ -23,8 +23,8 @@ export const listCategories = async (query?: Record<string, any>) => {
|
|||||||
cache: "force-cache",
|
cache: "force-cache",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
.then(({ product_categories }) => product_categories)
|
.then(({ product_categories }) => product_categories);
|
||||||
}
|
};
|
||||||
|
|
||||||
export const getCategoryByHandle = async (categoryHandle: string[]) => {
|
export const getCategoryByHandle = async (categoryHandle: string[]) => {
|
||||||
const { product_categories } = await getProductCategories({
|
const { product_categories } = await getProductCategories({
|
||||||
@@ -32,7 +32,7 @@ export const getCategoryByHandle = async (categoryHandle: string[]) => {
|
|||||||
limit: 1,
|
limit: 1,
|
||||||
});
|
});
|
||||||
return product_categories[0];
|
return product_categories[0];
|
||||||
}
|
};
|
||||||
|
|
||||||
export const getProductCategories = async ({
|
export const getProductCategories = async ({
|
||||||
handle,
|
handle,
|
||||||
@@ -45,19 +45,18 @@ export const getProductCategories = async ({
|
|||||||
} = {}) => {
|
} = {}) => {
|
||||||
const next = {
|
const next = {
|
||||||
...(await getCacheOptions("categories")),
|
...(await getCacheOptions("categories")),
|
||||||
}
|
};
|
||||||
|
|
||||||
return sdk.client
|
return sdk.client.fetch<HttpTypes.StoreProductCategoryListResponse>(
|
||||||
.fetch<HttpTypes.StoreProductCategoryListResponse>(
|
`/store/product-categories`,
|
||||||
`/store/product-categories`,
|
{
|
||||||
{
|
query: {
|
||||||
query: {
|
fields,
|
||||||
fields,
|
handle,
|
||||||
handle,
|
limit,
|
||||||
limit,
|
},
|
||||||
},
|
next,
|
||||||
next,
|
//cache: "force-cache",
|
||||||
//cache: "force-cache",
|
}
|
||||||
}
|
);
|
||||||
);
|
};
|
||||||
}
|
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ export default function ConfirmationModal({
|
|||||||
<Trans i18nKey={descriptionKey} />
|
<Trans i18nKey={descriptionKey} />
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<DialogFooter>
|
<DialogFooter className='gap-3'>
|
||||||
<Button variant="outline" onClick={onClose}>
|
<Button variant="outline" onClick={onClose}>
|
||||||
<Trans i18nKey={cancelKey} />
|
<Trans i18nKey={cancelKey} />
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ const PathsSchema = z.object({
|
|||||||
home: z.string().min(1),
|
home: z.string().min(1),
|
||||||
selectPackage: z.string().min(1),
|
selectPackage: z.string().min(1),
|
||||||
booking: z.string().min(1),
|
booking: z.string().min(1),
|
||||||
|
bookingHandle: z.string().min(1),
|
||||||
myOrders: z.string().min(1),
|
myOrders: z.string().min(1),
|
||||||
analysisResults: z.string().min(1),
|
analysisResults: z.string().min(1),
|
||||||
orderAnalysisPackage: z.string().min(1),
|
orderAnalysisPackage: z.string().min(1),
|
||||||
@@ -64,6 +65,7 @@ const pathsConfig = PathsSchema.parse({
|
|||||||
joinTeam: '/join',
|
joinTeam: '/join',
|
||||||
selectPackage: '/select-package',
|
selectPackage: '/select-package',
|
||||||
booking: '/home/booking',
|
booking: '/home/booking',
|
||||||
|
bookingHandle: '/home/booking/[handle]',
|
||||||
orderAnalysisPackage: '/home/order-analysis-package',
|
orderAnalysisPackage: '/home/order-analysis-package',
|
||||||
myOrders: '/home/order',
|
myOrders: '/home/order',
|
||||||
analysisResults: '/home/analysis-results',
|
analysisResults: '/home/analysis-results',
|
||||||
|
|||||||
@@ -108,6 +108,90 @@ export type Database = {
|
|||||||
}
|
}
|
||||||
Relationships: []
|
Relationships: []
|
||||||
}
|
}
|
||||||
|
medipost_dispatch: {
|
||||||
|
Row: {
|
||||||
|
changed_by: string | null
|
||||||
|
created_at: string
|
||||||
|
error_message: string | null
|
||||||
|
id: number
|
||||||
|
is_medipost_error: boolean
|
||||||
|
is_success: boolean
|
||||||
|
medusa_order_id: string
|
||||||
|
}
|
||||||
|
Insert: {
|
||||||
|
changed_by?: string | null
|
||||||
|
created_at?: string
|
||||||
|
error_message?: string | null
|
||||||
|
id?: number
|
||||||
|
is_medipost_error: boolean
|
||||||
|
is_success: boolean
|
||||||
|
medusa_order_id: string
|
||||||
|
}
|
||||||
|
Update: {
|
||||||
|
changed_by?: string | null
|
||||||
|
created_at?: string
|
||||||
|
error_message?: string | null
|
||||||
|
id?: number
|
||||||
|
is_medipost_error?: boolean
|
||||||
|
is_success?: boolean
|
||||||
|
medusa_order_id?: string
|
||||||
|
}
|
||||||
|
Relationships: []
|
||||||
|
}
|
||||||
|
medusa_action: {
|
||||||
|
Row: {
|
||||||
|
action: string
|
||||||
|
created_at: string
|
||||||
|
id: number
|
||||||
|
medusa_user_id: string
|
||||||
|
page: string | null
|
||||||
|
user_email: string
|
||||||
|
}
|
||||||
|
Insert: {
|
||||||
|
action: string
|
||||||
|
created_at?: string
|
||||||
|
id?: number
|
||||||
|
medusa_user_id: string
|
||||||
|
page?: string | null
|
||||||
|
user_email: string
|
||||||
|
}
|
||||||
|
Update: {
|
||||||
|
action?: string
|
||||||
|
created_at?: string
|
||||||
|
id?: number
|
||||||
|
medusa_user_id?: string
|
||||||
|
page?: string | null
|
||||||
|
user_email?: string
|
||||||
|
}
|
||||||
|
Relationships: []
|
||||||
|
}
|
||||||
|
notification_entries: {
|
||||||
|
Row: {
|
||||||
|
action: string
|
||||||
|
comment: string | null
|
||||||
|
created_at: string
|
||||||
|
id: number
|
||||||
|
related_record_key: string | null
|
||||||
|
status: Database["audit"]["Enums"]["action_status"]
|
||||||
|
}
|
||||||
|
Insert: {
|
||||||
|
action: string
|
||||||
|
comment?: string | null
|
||||||
|
created_at?: string
|
||||||
|
id?: number
|
||||||
|
related_record_key?: string | null
|
||||||
|
status: Database["audit"]["Enums"]["action_status"]
|
||||||
|
}
|
||||||
|
Update: {
|
||||||
|
action?: string
|
||||||
|
comment?: string | null
|
||||||
|
created_at?: string
|
||||||
|
id?: number
|
||||||
|
related_record_key?: string | null
|
||||||
|
status?: Database["audit"]["Enums"]["action_status"]
|
||||||
|
}
|
||||||
|
Relationships: []
|
||||||
|
}
|
||||||
page_views: {
|
page_views: {
|
||||||
Row: {
|
Row: {
|
||||||
account_id: string
|
account_id: string
|
||||||
@@ -201,28 +285,6 @@ export type Database = {
|
|||||||
}
|
}
|
||||||
Relationships: []
|
Relationships: []
|
||||||
}
|
}
|
||||||
medusa_action: {
|
|
||||||
Row: {
|
|
||||||
id: number
|
|
||||||
medusa_user_id: string
|
|
||||||
user_email: string
|
|
||||||
action: string
|
|
||||||
page: string
|
|
||||||
created_at: string
|
|
||||||
}
|
|
||||||
Insert: {
|
|
||||||
medusa_user_id: string
|
|
||||||
user_email: string
|
|
||||||
action: string
|
|
||||||
page: string
|
|
||||||
}
|
|
||||||
Update: {
|
|
||||||
medusa_user_id?: string
|
|
||||||
user_email?: string
|
|
||||||
action?: string
|
|
||||||
page?: string
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Views: {
|
Views: {
|
||||||
[_ in never]: never
|
[_ in never]: never
|
||||||
@@ -231,6 +293,7 @@ export type Database = {
|
|||||||
[_ in never]: never
|
[_ in never]: never
|
||||||
}
|
}
|
||||||
Enums: {
|
Enums: {
|
||||||
|
action_status: "SUCCESS" | "FAIL"
|
||||||
doctor_page_view_action:
|
doctor_page_view_action:
|
||||||
| "VIEW_ANALYSIS_RESULTS"
|
| "VIEW_ANALYSIS_RESULTS"
|
||||||
| "VIEW_DASHBOARD"
|
| "VIEW_DASHBOARD"
|
||||||
@@ -329,14 +392,14 @@ export type Database = {
|
|||||||
id: string
|
id: string
|
||||||
is_personal_account: boolean
|
is_personal_account: boolean
|
||||||
last_name: string | null
|
last_name: string | null
|
||||||
|
medusa_account_id: string | null
|
||||||
name: string
|
name: string
|
||||||
personal_code: string | null
|
personal_code: string | null
|
||||||
phone: string | null
|
phone: string | null
|
||||||
picture_url: string | null
|
picture_url: string | null
|
||||||
|
preferred_locale: Database["medreport"]["Enums"]["locale"] | null
|
||||||
primary_owner_user_id: string
|
primary_owner_user_id: string
|
||||||
public_data: Json
|
|
||||||
slug: string | null
|
slug: string | null
|
||||||
medusa_account_id: string | null
|
|
||||||
updated_at: string | null
|
updated_at: string | null
|
||||||
updated_by: string | null
|
updated_by: string | null
|
||||||
}
|
}
|
||||||
@@ -351,14 +414,14 @@ export type Database = {
|
|||||||
id?: string
|
id?: string
|
||||||
is_personal_account?: boolean
|
is_personal_account?: boolean
|
||||||
last_name?: string | null
|
last_name?: string | null
|
||||||
|
medusa_account_id?: string | null
|
||||||
name: string
|
name: string
|
||||||
personal_code?: string | null
|
personal_code?: string | null
|
||||||
phone?: string | null
|
phone?: string | null
|
||||||
picture_url?: string | null
|
picture_url?: string | null
|
||||||
|
preferred_locale?: Database["medreport"]["Enums"]["locale"] | null
|
||||||
primary_owner_user_id?: string
|
primary_owner_user_id?: string
|
||||||
public_data?: Json
|
|
||||||
slug?: string | null
|
slug?: string | null
|
||||||
medusa_account_id?: string | null
|
|
||||||
updated_at?: string | null
|
updated_at?: string | null
|
||||||
updated_by?: string | null
|
updated_by?: string | null
|
||||||
}
|
}
|
||||||
@@ -373,14 +436,14 @@ export type Database = {
|
|||||||
id?: string
|
id?: string
|
||||||
is_personal_account?: boolean
|
is_personal_account?: boolean
|
||||||
last_name?: string | null
|
last_name?: string | null
|
||||||
|
medusa_account_id?: string | null
|
||||||
name?: string
|
name?: string
|
||||||
personal_code?: string | null
|
personal_code?: string | null
|
||||||
phone?: string | null
|
phone?: string | null
|
||||||
picture_url?: string | null
|
picture_url?: string | null
|
||||||
|
preferred_locale?: Database["medreport"]["Enums"]["locale"] | null
|
||||||
primary_owner_user_id?: string
|
primary_owner_user_id?: string
|
||||||
public_data?: Json
|
|
||||||
slug?: string | null
|
slug?: string | null
|
||||||
medusa_account_id?: string | null
|
|
||||||
updated_at?: string | null
|
updated_at?: string | null
|
||||||
updated_by?: string | null
|
updated_by?: string | null
|
||||||
}
|
}
|
||||||
@@ -393,6 +456,7 @@ export type Database = {
|
|||||||
created_at: string
|
created_at: string
|
||||||
created_by: string | null
|
created_by: string | null
|
||||||
has_seen_confirmation: boolean
|
has_seen_confirmation: boolean
|
||||||
|
id: string
|
||||||
updated_at: string
|
updated_at: string
|
||||||
updated_by: string | null
|
updated_by: string | null
|
||||||
user_id: string
|
user_id: string
|
||||||
@@ -403,6 +467,7 @@ export type Database = {
|
|||||||
created_at?: string
|
created_at?: string
|
||||||
created_by?: string | null
|
created_by?: string | null
|
||||||
has_seen_confirmation?: boolean
|
has_seen_confirmation?: boolean
|
||||||
|
id?: string
|
||||||
updated_at?: string
|
updated_at?: string
|
||||||
updated_by?: string | null
|
updated_by?: string | null
|
||||||
user_id: string
|
user_id: string
|
||||||
@@ -413,6 +478,7 @@ export type Database = {
|
|||||||
created_at?: string
|
created_at?: string
|
||||||
created_by?: string | null
|
created_by?: string | null
|
||||||
has_seen_confirmation?: boolean
|
has_seen_confirmation?: boolean
|
||||||
|
id?: string
|
||||||
updated_at?: string
|
updated_at?: string
|
||||||
updated_by?: string | null
|
updated_by?: string | null
|
||||||
user_id?: string
|
user_id?: string
|
||||||
@@ -1022,7 +1088,7 @@ export type Database = {
|
|||||||
price: number
|
price: number
|
||||||
price_periods: string | null
|
price_periods: string | null
|
||||||
requires_payment: boolean
|
requires_payment: boolean
|
||||||
sync_id: number
|
sync_id: string | null
|
||||||
updated_at: string | null
|
updated_at: string | null
|
||||||
}
|
}
|
||||||
Insert: {
|
Insert: {
|
||||||
@@ -1041,7 +1107,7 @@ export type Database = {
|
|||||||
price: number
|
price: number
|
||||||
price_periods?: string | null
|
price_periods?: string | null
|
||||||
requires_payment: boolean
|
requires_payment: boolean
|
||||||
sync_id: number
|
sync_id?: string | null
|
||||||
updated_at?: string | null
|
updated_at?: string | null
|
||||||
}
|
}
|
||||||
Update: {
|
Update: {
|
||||||
@@ -1060,7 +1126,7 @@ export type Database = {
|
|||||||
price?: number
|
price?: number
|
||||||
price_periods?: string | null
|
price_periods?: string | null
|
||||||
requires_payment?: boolean
|
requires_payment?: boolean
|
||||||
sync_id?: number
|
sync_id?: string | null
|
||||||
updated_at?: string | null
|
updated_at?: string | null
|
||||||
}
|
}
|
||||||
Relationships: [
|
Relationships: [
|
||||||
@@ -1081,7 +1147,7 @@ export type Database = {
|
|||||||
doctor_user_id: string | null
|
doctor_user_id: string | null
|
||||||
id: number
|
id: number
|
||||||
status: Database["medreport"]["Enums"]["analysis_feedback_status"]
|
status: Database["medreport"]["Enums"]["analysis_feedback_status"]
|
||||||
updated_at: string
|
updated_at: string | null
|
||||||
updated_by: string | null
|
updated_by: string | null
|
||||||
user_id: string
|
user_id: string
|
||||||
value: string | null
|
value: string | null
|
||||||
@@ -1093,7 +1159,7 @@ export type Database = {
|
|||||||
doctor_user_id?: string | null
|
doctor_user_id?: string | null
|
||||||
id?: number
|
id?: number
|
||||||
status?: Database["medreport"]["Enums"]["analysis_feedback_status"]
|
status?: Database["medreport"]["Enums"]["analysis_feedback_status"]
|
||||||
updated_at?: string
|
updated_at?: string | null
|
||||||
updated_by?: string | null
|
updated_by?: string | null
|
||||||
user_id: string
|
user_id: string
|
||||||
value?: string | null
|
value?: string | null
|
||||||
@@ -1105,7 +1171,7 @@ export type Database = {
|
|||||||
doctor_user_id?: string | null
|
doctor_user_id?: string | null
|
||||||
id?: number
|
id?: number
|
||||||
status?: Database["medreport"]["Enums"]["analysis_feedback_status"]
|
status?: Database["medreport"]["Enums"]["analysis_feedback_status"]
|
||||||
updated_at?: string
|
updated_at?: string | null
|
||||||
updated_by?: string | null
|
updated_by?: string | null
|
||||||
user_id?: string
|
user_id?: string
|
||||||
value?: string | null
|
value?: string | null
|
||||||
@@ -1784,9 +1850,7 @@ export type Database = {
|
|||||||
Returns: Json
|
Returns: Json
|
||||||
}
|
}
|
||||||
create_team_account: {
|
create_team_account: {
|
||||||
Args:
|
Args: { account_name: string; new_personal_code: string }
|
||||||
| { account_name: string }
|
|
||||||
| { account_name: string; new_personal_code: string }
|
|
||||||
Returns: {
|
Returns: {
|
||||||
application_role: Database["medreport"]["Enums"]["application_role"]
|
application_role: Database["medreport"]["Enums"]["application_role"]
|
||||||
city: string | null
|
city: string | null
|
||||||
@@ -1798,12 +1862,13 @@ export type Database = {
|
|||||||
id: string
|
id: string
|
||||||
is_personal_account: boolean
|
is_personal_account: boolean
|
||||||
last_name: string | null
|
last_name: string | null
|
||||||
|
medusa_account_id: string | null
|
||||||
name: string
|
name: string
|
||||||
personal_code: string | null
|
personal_code: string | null
|
||||||
phone: string | null
|
phone: string | null
|
||||||
picture_url: string | null
|
picture_url: string | null
|
||||||
|
preferred_locale: Database["medreport"]["Enums"]["locale"] | null
|
||||||
primary_owner_user_id: string
|
primary_owner_user_id: string
|
||||||
public_data: Json
|
|
||||||
slug: string | null
|
slug: string | null
|
||||||
updated_at: string | null
|
updated_at: string | null
|
||||||
updated_by: string | null
|
updated_by: string | null
|
||||||
@@ -1836,6 +1901,7 @@ export type Database = {
|
|||||||
primary_owner_user_id: string
|
primary_owner_user_id: string
|
||||||
name: string
|
name: string
|
||||||
email: string
|
email: string
|
||||||
|
personal_code: string
|
||||||
picture_url: string
|
picture_url: string
|
||||||
created_at: string
|
created_at: string
|
||||||
updated_at: string
|
updated_at: string
|
||||||
@@ -1853,10 +1919,18 @@ export type Database = {
|
|||||||
account_id: string
|
account_id: string
|
||||||
}[]
|
}[]
|
||||||
}
|
}
|
||||||
|
get_medipost_dispatch_tries: {
|
||||||
|
Args: { p_medusa_order_id: string }
|
||||||
|
Returns: number
|
||||||
|
}
|
||||||
get_nonce_status: {
|
get_nonce_status: {
|
||||||
Args: { p_id: string }
|
Args: { p_id: string }
|
||||||
Returns: Json
|
Returns: Json
|
||||||
}
|
}
|
||||||
|
get_order_possible_actions: {
|
||||||
|
Args: { p_medusa_order_id: string }
|
||||||
|
Returns: Json
|
||||||
|
}
|
||||||
get_upper_system_role: {
|
get_upper_system_role: {
|
||||||
Args: Record<PropertyKey, never>
|
Args: Record<PropertyKey, never>
|
||||||
Returns: string
|
Returns: string
|
||||||
@@ -1937,6 +2011,10 @@ export type Database = {
|
|||||||
Args: { account_id: string; user_id: string }
|
Args: { account_id: string; user_id: string }
|
||||||
Returns: boolean
|
Returns: boolean
|
||||||
}
|
}
|
||||||
|
medipost_retry_dispatch: {
|
||||||
|
Args: { order_id: string }
|
||||||
|
Returns: Json
|
||||||
|
}
|
||||||
revoke_nonce: {
|
revoke_nonce: {
|
||||||
Args: { p_id: string; p_reason?: string }
|
Args: { p_id: string; p_reason?: string }
|
||||||
Returns: boolean
|
Returns: boolean
|
||||||
@@ -2057,21 +2135,6 @@ export type Database = {
|
|||||||
}
|
}
|
||||||
Returns: Json
|
Returns: Json
|
||||||
}
|
}
|
||||||
medipost_retry_dispatch: {
|
|
||||||
Args: {
|
|
||||||
order_id: string
|
|
||||||
}
|
|
||||||
Returns: {
|
|
||||||
success: boolean
|
|
||||||
error: string | null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
get_medipost_dispatch_tries: {
|
|
||||||
Args: {
|
|
||||||
p_medusa_order_id: string
|
|
||||||
}
|
|
||||||
Returns: number
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Enums: {
|
Enums: {
|
||||||
analysis_feedback_status: "STARTED" | "DRAFT" | "COMPLETED"
|
analysis_feedback_status: "STARTED" | "DRAFT" | "COMPLETED"
|
||||||
@@ -2093,6 +2156,7 @@ export type Database = {
|
|||||||
| "invites.manage"
|
| "invites.manage"
|
||||||
application_role: "user" | "doctor" | "super_admin"
|
application_role: "user" | "doctor" | "super_admin"
|
||||||
billing_provider: "stripe" | "lemon-squeezy" | "paddle" | "montonio"
|
billing_provider: "stripe" | "lemon-squeezy" | "paddle" | "montonio"
|
||||||
|
locale: "en" | "et" | "ru"
|
||||||
notification_channel: "in_app" | "email"
|
notification_channel: "in_app" | "email"
|
||||||
notification_type: "info" | "warning" | "error"
|
notification_type: "info" | "warning" | "error"
|
||||||
payment_status: "pending" | "succeeded" | "failed"
|
payment_status: "pending" | "succeeded" | "failed"
|
||||||
@@ -7959,6 +8023,7 @@ export type CompositeTypes<
|
|||||||
export const Constants = {
|
export const Constants = {
|
||||||
audit: {
|
audit: {
|
||||||
Enums: {
|
Enums: {
|
||||||
|
action_status: ["SUCCESS", "FAIL"],
|
||||||
doctor_page_view_action: [
|
doctor_page_view_action: [
|
||||||
"VIEW_ANALYSIS_RESULTS",
|
"VIEW_ANALYSIS_RESULTS",
|
||||||
"VIEW_DASHBOARD",
|
"VIEW_DASHBOARD",
|
||||||
@@ -7996,6 +8061,7 @@ export const Constants = {
|
|||||||
],
|
],
|
||||||
application_role: ["user", "doctor", "super_admin"],
|
application_role: ["user", "doctor", "super_admin"],
|
||||||
billing_provider: ["stripe", "lemon-squeezy", "paddle", "montonio"],
|
billing_provider: ["stripe", "lemon-squeezy", "paddle", "montonio"],
|
||||||
|
locale: ["en", "et", "ru"],
|
||||||
notification_channel: ["in_app", "email"],
|
notification_channel: ["in_app", "email"],
|
||||||
notification_type: ["info", "warning", "error"],
|
notification_type: ["info", "warning", "error"],
|
||||||
payment_status: ["pending", "succeeded", "failed"],
|
payment_status: ["pending", "succeeded", "failed"],
|
||||||
|
|||||||
@@ -1,16 +1,22 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useCallback, useMemo, useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
|
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
|
import { useRevalidatePersonalAccountDataQuery } from '@kit/accounts/hooks/use-personal-account-data';
|
||||||
|
import { useUpdateAccountData } from '@kit/accounts/hooks/use-update-account';
|
||||||
|
import { Database } from '@kit/supabase/database';
|
||||||
|
import { useUser } from '@kit/supabase/hooks/use-user';
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
SelectContent,
|
SelectContent,
|
||||||
SelectItem,
|
SelectItem,
|
||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from '../shadcn/select';
|
} from '@kit/ui/select';
|
||||||
|
import { Trans } from '@kit/ui/trans';
|
||||||
|
|
||||||
export function LanguageSelector({
|
export function LanguageSelector({
|
||||||
onChange,
|
onChange,
|
||||||
@@ -19,6 +25,9 @@ export function LanguageSelector({
|
|||||||
}) {
|
}) {
|
||||||
const { i18n } = useTranslation();
|
const { i18n } = useTranslation();
|
||||||
const { language: currentLanguage, options } = i18n;
|
const { language: currentLanguage, options } = i18n;
|
||||||
|
const [value, setValue] = useState(i18n.language);
|
||||||
|
|
||||||
|
const { data: user } = useUser();
|
||||||
|
|
||||||
const locales = (options.supportedLngs as string[]).filter(
|
const locales = (options.supportedLngs as string[]).filter(
|
||||||
(locale) => locale.toLowerCase() !== 'cimode',
|
(locale) => locale.toLowerCase() !== 'cimode',
|
||||||
@@ -30,26 +39,37 @@ export function LanguageSelector({
|
|||||||
});
|
});
|
||||||
}, [currentLanguage]);
|
}, [currentLanguage]);
|
||||||
|
|
||||||
const [value, setValue] = useState(i18n.language);
|
const userId = user?.id;
|
||||||
|
const updateAccountMutation = useUpdateAccountData(userId!);
|
||||||
|
const revalidateUserDataQuery = useRevalidatePersonalAccountDataQuery();
|
||||||
|
|
||||||
const languageChanged = useCallback(
|
const updateLanguagePreference = async (
|
||||||
async (locale: string) => {
|
locale: Database['medreport']['Enums']['locale'],
|
||||||
setValue(locale);
|
) => {
|
||||||
|
setValue(locale);
|
||||||
|
|
||||||
if (onChange) {
|
if (onChange) {
|
||||||
onChange(locale);
|
onChange(locale);
|
||||||
}
|
}
|
||||||
|
|
||||||
await i18n.changeLanguage(locale);
|
const promise = updateAccountMutation
|
||||||
|
.mutateAsync({
|
||||||
|
preferred_locale: locale,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
revalidateUserDataQuery(userId!);
|
||||||
|
});
|
||||||
|
await i18n.changeLanguage(locale);
|
||||||
|
|
||||||
// refresh cached translations
|
return toast.promise(() => promise, {
|
||||||
window.location.reload();
|
success: <Trans i18nKey={'account:updatePreferredLocaleSuccess'} />,
|
||||||
},
|
error: <Trans i18nKey={'account:updatePreferredLocaleError'} />,
|
||||||
[i18n, onChange],
|
loading: <Trans i18nKey={'account:updatePreferredLocaleLoading'} />,
|
||||||
);
|
});
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Select value={value} onValueChange={languageChanged}>
|
<Select value={value} onValueChange={updateLanguagePreference}>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
|
|||||||
@@ -125,5 +125,8 @@
|
|||||||
},
|
},
|
||||||
"updateRoleSuccess": "Role updated",
|
"updateRoleSuccess": "Role updated",
|
||||||
"updateRoleError": "Something went wrong, please try again",
|
"updateRoleError": "Something went wrong, please try again",
|
||||||
"updateRoleLoading": "Updating role..."
|
"updateRoleLoading": "Updating role...",
|
||||||
|
"updatePreferredLocaleSuccess": "Language preference updated",
|
||||||
|
"updatePreferredLocaleError": "Language preference update failed",
|
||||||
|
"updatePreferredLocaleLoading": "Updating language preference..."
|
||||||
}
|
}
|
||||||
@@ -9,8 +9,7 @@
|
|||||||
},
|
},
|
||||||
"status": {
|
"status": {
|
||||||
"QUEUED": "Waiting to send to lab",
|
"QUEUED": "Waiting to send to lab",
|
||||||
"ON_HOLD": "Waiting for analysis results",
|
"PROCESSING": "Waiting for results",
|
||||||
"PROCESSING": "In progress",
|
|
||||||
"PARTIAL_ANALYSIS_RESPONSE": "Partial analysis response",
|
"PARTIAL_ANALYSIS_RESPONSE": "Partial analysis response",
|
||||||
"FULL_ANALYSIS_RESPONSE": "All analysis responses received, waiting for doctor response",
|
"FULL_ANALYSIS_RESPONSE": "All analysis responses received, waiting for doctor response",
|
||||||
"COMPLETED": "Completed",
|
"COMPLETED": "Completed",
|
||||||
|
|||||||
@@ -148,5 +148,8 @@
|
|||||||
},
|
},
|
||||||
"updateRoleSuccess": "Roll uuendatud",
|
"updateRoleSuccess": "Roll uuendatud",
|
||||||
"updateRoleError": "Midagi läks valesti. Palun proovi uuesti",
|
"updateRoleError": "Midagi läks valesti. Palun proovi uuesti",
|
||||||
"updateRoleLoading": "Rolli uuendatakse..."
|
"updateRoleLoading": "Rolli uuendatakse...",
|
||||||
|
"updatePreferredLocaleSuccess": "Eelistatud keel uuendatud",
|
||||||
|
"updatePreferredLocaleError": "Eelistatud keele uuendamine ei õnnestunud",
|
||||||
|
"updatePreferredLocaleLoading": "Eelistatud keelt uuendatakse..."
|
||||||
}
|
}
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
{
|
{
|
||||||
"title": "Vali teenus",
|
"title": "Vali teenus",
|
||||||
"description": "Vali sobiv teenus või pakett vastavalt oma tervisemurele või -eesmärgile.",
|
"description": "Vali sobiv teenus või pakett vastavalt oma tervisemurele või -eesmärgile.",
|
||||||
"analysisPackages": {
|
"analysisPackages": {
|
||||||
"title": "Analüüside paketid",
|
"title": "Analüüside paketid",
|
||||||
"description": "Tutvu personaalsete analüüsi pakettidega ja telli"
|
"description": "Tutvu personaalsete analüüsi pakettidega ja telli"
|
||||||
}
|
},
|
||||||
|
"noCategories": "Teenuste loetelu ei leitud, proovi hiljem uuesti"
|
||||||
}
|
}
|
||||||
@@ -80,7 +80,8 @@
|
|||||||
"dashboard": "Ülevaade",
|
"dashboard": "Ülevaade",
|
||||||
"settings": "Settings",
|
"settings": "Settings",
|
||||||
"profile": "Profile",
|
"profile": "Profile",
|
||||||
"application": "Application"
|
"application": "Application",
|
||||||
|
"pickTime": "Vali aeg"
|
||||||
},
|
},
|
||||||
"roles": {
|
"roles": {
|
||||||
"owner": {
|
"owner": {
|
||||||
|
|||||||
@@ -9,7 +9,6 @@
|
|||||||
},
|
},
|
||||||
"status": {
|
"status": {
|
||||||
"QUEUED": "Esitatud",
|
"QUEUED": "Esitatud",
|
||||||
"ON_HOLD": "Makstud",
|
|
||||||
"PROCESSING": "Synlabile edastatud",
|
"PROCESSING": "Synlabile edastatud",
|
||||||
"PARTIAL_ANALYSIS_RESPONSE": "Osalised tulemused",
|
"PARTIAL_ANALYSIS_RESPONSE": "Osalised tulemused",
|
||||||
"FULL_ANALYSIS_RESPONSE": "Kõik tulemused käes, ootab arsti kokkuvõtet",
|
"FULL_ANALYSIS_RESPONSE": "Kõik tulemused käes, ootab arsti kokkuvõtet",
|
||||||
|
|||||||
@@ -125,5 +125,8 @@
|
|||||||
},
|
},
|
||||||
"updateRoleSuccess": "Role updated",
|
"updateRoleSuccess": "Role updated",
|
||||||
"updateRoleError": "Something went wrong, please try again",
|
"updateRoleError": "Something went wrong, please try again",
|
||||||
"updateRoleLoading": "Updating role..."
|
"updateRoleLoading": "Updating role...",
|
||||||
|
"updatePreferredLocaleSuccess": "Language preference updated",
|
||||||
|
"updatePreferredLocaleError": "Language preference update failed",
|
||||||
|
"updatePreferredLocaleLoading": "Updating language preference..."
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
ALTER TABLE medreport.accounts
|
||||||
|
DROP COLUMN IF EXISTS public_data;
|
||||||
|
|
||||||
|
create type medreport.locale as enum ('en', 'et', 'ru');
|
||||||
|
|
||||||
|
ALTER TABLE medreport.accounts
|
||||||
|
ADD COLUMN preferred_locale medreport.locale
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
create type "audit"."action_status" as enum ('SUCCESS', 'FAIL');
|
||||||
|
|
||||||
|
create table audit.notification_entries (
|
||||||
|
"id" bigint generated by default as identity not null,
|
||||||
|
"status" audit.action_status not null,
|
||||||
|
"action" text not null,
|
||||||
|
"comment" text,
|
||||||
|
"related_record_key" text,
|
||||||
|
"created_at" timestamp with time zone not null default now()
|
||||||
|
);
|
||||||
|
|
||||||
|
grant usage on schema audit to authenticated;
|
||||||
|
grant select, insert on table audit.notification_entries to authenticated;
|
||||||
2
supabase/migrations/20250827134000_bookings.sql
Normal file
2
supabase/migrations/20250827134000_bookings.sql
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE medreport.connected_online_services
|
||||||
|
ALTER COLUMN sync_id TYPE text USING sync_id::text;
|
||||||
Reference in New Issue
Block a user