Merge branch 'main' into MED-85

This commit is contained in:
2025-08-27 08:31:26 +03:00
55 changed files with 1356 additions and 547 deletions

View File

@@ -1,9 +1,8 @@
import { AppLogo } from '@kit/shared/components/app-logo';
import { appConfig } from '@kit/shared/config';
import { Footer } from '@kit/ui/marketing'; import { Footer } from '@kit/ui/marketing';
import { Trans } from '@kit/ui/trans'; import { Trans } from '@kit/ui/trans';
import { AppLogo } from '@kit/shared/components/app-logo';
import appConfig from '@kit/shared/config/app.config';
export function SiteFooter() { export function SiteFooter() {
return ( return (
<Footer <Footer

View File

@@ -5,7 +5,7 @@ import { getSupabaseServerClient } from '@kit/supabase/server-client';
import { withI18n } from '~/lib/i18n/with-i18n'; import { withI18n } from '~/lib/i18n/with-i18n';
import { import {
PAGE_VIEW_ACTION, PageViewAction,
createPageViewLog, createPageViewLog,
} from '~/lib/services/audit/pageView.service'; } from '~/lib/services/audit/pageView.service';
@@ -23,7 +23,7 @@ async function MembershipConfirmation() {
} }
await createPageViewLog({ await createPageViewLog({
accountId: user.id, accountId: user.id,
action: PAGE_VIEW_ACTION.REGISTRATION_SUCCESS, action: PageViewAction.REGISTRATION_SUCCESS,
}); });
return <MembershipConfirmationNotification userId={user.id} />; return <MembershipConfirmationNotification userId={user.id} />;

View File

@@ -19,7 +19,7 @@ import {
} from '@kit/doctor/schema/doctor-analysis-detail-view.schema'; } from '@kit/doctor/schema/doctor-analysis-detail-view.schema';
import { import {
DoctorAnalysisFeedbackForm, DoctorAnalysisFeedbackForm,
doctorAnalysisFeedbackSchema, doctorAnalysisFeedbackFormSchema,
} from '@kit/doctor/schema/doctor-analysis.schema'; } from '@kit/doctor/schema/doctor-analysis.schema';
import ConfirmationModal from '@kit/shared/components/confirmation-modal'; import ConfirmationModal from '@kit/shared/components/confirmation-modal';
import { getFullName } from '@kit/shared/utils'; import { getFullName } from '@kit/shared/utils';
@@ -36,9 +36,11 @@ import { toast } from '@kit/ui/sonner';
import { Textarea } from '@kit/ui/textarea'; import { Textarea } from '@kit/ui/textarea';
import { Trans } from '@kit/ui/trans'; import { Trans } from '@kit/ui/trans';
import Analysis from '~/home/(user)/(dashboard)/analysis-results/_components/analysis';
import { bmiFromMetric } from '~/lib/utils'; import { bmiFromMetric } from '~/lib/utils';
import DoctorAnalysisWrapper from './doctor-analysis-wrapper';
import DoctorJobSelect from './doctor-job-select';
export default function AnalysisView({ export default function AnalysisView({
patient, patient,
order, order,
@@ -54,16 +56,20 @@ export default function AnalysisView({
const { data: user } = useUser(); const { data: user } = useUser();
const isInProgress = const isInProgress = !!(
!!feedback?.status && !!feedback?.status &&
feedback?.doctor_user_id && feedback?.doctor_user_id &&
feedback?.status !== 'COMPLETED'; feedback?.status !== 'COMPLETED'
);
const isCurrentDoctorJob =
!!feedback?.doctor_user_id && feedback?.doctor_user_id === user?.id;
const isReadOnly = const isReadOnly =
!isInProgress || !isInProgress ||
(!!feedback?.doctor_user_id && feedback?.doctor_user_id !== user?.id); (!!feedback?.doctor_user_id && feedback?.doctor_user_id !== user?.id);
const form = useForm({ const form = useForm({
resolver: zodResolver(doctorAnalysisFeedbackSchema), resolver: zodResolver(doctorAnalysisFeedbackFormSchema),
reValidateMode: 'onChange',
defaultValues: { defaultValues: {
feedbackValue: feedback?.value ?? '', feedbackValue: feedback?.value ?? '',
userId: patient.userId, userId: patient.userId,
@@ -80,35 +86,41 @@ export default function AnalysisView({
data: DoctorAnalysisFeedbackForm, data: DoctorAnalysisFeedbackForm,
status: 'DRAFT' | 'COMPLETED', status: 'DRAFT' | 'COMPLETED',
) => { ) => {
try { const result = await giveFeedbackAction({
const feedbackPromise = giveFeedbackAction({ ...data,
...data, analysisOrderId: order.analysisOrderId,
analysisOrderId: order.analysisOrderId, status,
status, });
});
toast.promise(() => feedbackPromise, { if (!result.success) {
success: <Trans i18nKey={'doctor:updateFeedbackSuccess'} />, return toast.error(<Trans i18nKey="common:genericServerError" />);
error: <Trans i18nKey={'doctor:updateFeedbackError'} />,
loading: <Trans i18nKey={'doctor:updateFeedbackLoading'} />,
});
queryClient.invalidateQueries({
predicate: (query) => query.queryKey.includes('doctor-jobs'),
});
return setIsConfirmOpen(false);
} catch (error) {
toast.error(<Trans i18nKey="common:genericServerError" />);
} }
queryClient.invalidateQueries({
predicate: (query) => query.queryKey.includes('doctor-jobs'),
});
toast.success(<Trans i18nKey={'doctor:updateFeedbackSuccess'} />);
return setIsConfirmOpen(false);
}; };
const handleDraftSubmit = () => { const handleDraftSubmit = async (e: React.FormEvent) => {
e.preventDefault();
form.formState.errors.feedbackValue = undefined;
const formData = form.getValues(); const formData = form.getValues();
onSubmit(formData, 'DRAFT'); onSubmit(formData, 'DRAFT');
}; };
const handleCompleteSubmit = () => { const handleCompleteSubmit = async (e: React.FormEvent) => {
e.preventDefault();
const isValid = await form.trigger();
if (!isValid) {
return;
}
setIsConfirmOpen(true); setIsConfirmOpen(true);
}; };
@@ -119,16 +131,31 @@ export default function AnalysisView({
return ( return (
<> <>
<h3> <div className="xs:flex xs:justify-between">
<Trans <h3>
i18nKey={getResultSetName( <Trans
order.title, i18nKey={getResultSetName(
order.isPackage, order.title,
Object.keys(analyses)?.length, order.isPackage,
)} Object.keys(analyses)?.length,
/> )}
</h3> />
<div className="grid grid-cols-2"> </h3>
<div className="xs:flex hidden">
<DoctorJobSelect
analysisOrderId={order.analysisOrderId}
userId={patient.userId}
doctorUserId={feedback?.doctor_user_id}
isRemovable={isCurrentDoctorJob && isInProgress}
onJobUpdate={() =>
queryClient.invalidateQueries({
predicate: (query) => query.queryKey.includes('doctor-jobs'),
})
}
/>
</div>
</div>
<div className="xs:grid-cols-2 grid">
<div className="font-bold"> <div className="font-bold">
<Trans i18nKey="doctor:name" /> <Trans i18nKey="doctor:name" />
</div> </div>
@@ -152,11 +179,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?.height ?? 0, patient?.weight ?? 0)}</div> <div>{bmiFromMetric(patient?.weight ?? 0, patient?.height ?? 0)}</div>
<div className="font-bold"> <div className="font-bold">
<Trans i18nKey="doctor:smoking" /> <Trans i18nKey="doctor:smoking" />
</div> </div>
<div></div> <div>-</div>
<div className="font-bold"> <div className="font-bold">
<Trans i18nKey="doctor:phone" /> <Trans i18nKey="doctor:phone" />
</div> </div>
@@ -166,30 +193,37 @@ export default function AnalysisView({
</div> </div>
<div>{patient.email}</div> <div>{patient.email}</div>
</div> </div>
<div className="xs:hidden block">
<DoctorJobSelect
className="w-full"
analysisOrderId={order.analysisOrderId}
userId={patient.userId}
doctorUserId={feedback?.doctor_user_id}
isRemovable={isCurrentDoctorJob && isInProgress}
onJobUpdate={() =>
queryClient.invalidateQueries({
predicate: (query) => query.queryKey.includes('doctor-jobs'),
})
}
/>
</div>
<h3> <h3>
<Trans i18nKey="doctor:results" /> <Trans i18nKey="doctor:results" />
</h3> </h3>
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
{analyses.map((analysisData) => { {analyses.map((analysisData) => {
return ( return (
<Analysis <DoctorAnalysisWrapper
key={analysisData.id} key={analysisData.id}
analysisElement={{ analysisData={analysisData}
analysis_name_lab: analysisData.analysis_name,
}}
results={analysisData}
/> />
); );
})} })}
</div> </div>
<h3> <h3>
<Trans i18nKey="doctor:feedback" /> <Trans i18nKey="doctor:feedback" />
</h3> </h3>
<p>{feedback?.value ?? '-'}</p> <p>{feedback?.value ?? '-'}</p>
{!isReadOnly && ( {!isReadOnly && (
<Form {...form}> <Form {...form}>
<form className="space-y-4 lg:w-1/2"> <form className="space-y-4 lg:w-1/2">
@@ -206,23 +240,21 @@ export default function AnalysisView({
)} )}
/> />
<div className="flex gap-2"> <div className="xs:flex block justify-end gap-2 space-y-2">
<Button <Button
type="button"
variant="outline" variant="outline"
onClick={(e) => { onClick={handleDraftSubmit}
e.preventDefault();
handleDraftSubmit();
}}
disabled={isReadOnly} disabled={isReadOnly}
className="xs:w-1/4 w-full"
> >
<Trans i18nKey="common:saveAsDraft" /> <Trans i18nKey="common:saveAsDraft" />
</Button> </Button>
<Button <Button
onClick={(e) => { type="button"
e.preventDefault(); onClick={handleCompleteSubmit}
handleCompleteSubmit();
}}
disabled={isReadOnly} disabled={isReadOnly}
className="xs:w-1/4 w-full"
> >
<Trans i18nKey="common:save" /> <Trans i18nKey="common:save" />
</Button> </Button>

View File

@@ -0,0 +1,103 @@
'use client';
import { CaretDownIcon, QuestionMarkCircledIcon } from '@radix-ui/react-icons';
import { useTranslation } from 'react-i18next';
import { AnalysisResponse } from '@kit/doctor/schema/doctor-analysis-detail-view.schema';
import { InfoTooltip } from '@kit/shared/components/ui/info-tooltip';
import { formatDate } from '@kit/shared/utils';
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from '@kit/ui/collapsible';
import { Trans } from '@kit/ui/trans';
import Analysis from '~/home/(user)/(dashboard)/analysis-results/_components/analysis';
export default function DoctorAnalysisWrapper({
analysisData,
}: {
analysisData: AnalysisResponse;
}) {
const { t } = useTranslation();
return (
<Collapsible className="w-full" key={analysisData.id}>
<CollapsibleTrigger
disabled={!analysisData.latestPreviousAnalysis}
asChild
>
<div className="[&[data-state=open]_.caret-icon]:rotate-180">
<Analysis
startIcon={
analysisData.latestPreviousAnalysis && (
<CaretDownIcon className="caret-icon transition-transform duration-200" />
)
}
endIcon={
analysisData.comment && (
<>
<div className="xs:flex hidden">
<InfoTooltip
content={analysisData.comment}
icon={
<QuestionMarkCircledIcon className="mx-2 text-blue-800" />
}
/>
</div>
<p className="xs:hidden">
<strong>
<Trans i18nKey="doctor:labComment" />:
</strong>{' '}
{analysisData.comment}
</p>
</>
)
}
analysisElement={{
analysis_name_lab: analysisData.analysis_name,
}}
results={analysisData}
/>
</div>
</CollapsibleTrigger>
{analysisData.latestPreviousAnalysis && (
<CollapsibleContent>
<div className="my-1 flex flex-col">
<Analysis
endIcon={
analysisData.latestPreviousAnalysis.comment && (
<>
<div className="xs:flex hidden">
<InfoTooltip
content={analysisData.latestPreviousAnalysis.comment}
icon={
<QuestionMarkCircledIcon className="mx-2 text-blue-800" />
}
/>
</div>
<p className="xs:hidden">
<strong>
<Trans i18nKey="doctor:labComment" />:{' '}
</strong>
{analysisData.latestPreviousAnalysis.comment}
</p>
</>
)
}
analysisElement={{
analysis_name_lab: t('doctor:previousResults', {
date: formatDate(
analysisData.latestPreviousAnalysis.response_time,
),
}),
}}
results={analysisData.latestPreviousAnalysis}
/>
</div>
</CollapsibleContent>
)}
</Collapsible>
);
}

View File

@@ -0,0 +1,120 @@
'use client';
import { useTransition } from 'react';
import { useRouter } from 'next/navigation';
import { LoaderCircle } from 'lucide-react';
import {
selectJobAction,
unselectJobAction,
} from '@kit/doctor/actions/doctor-server-actions';
import { ErrorReason } from '@kit/doctor/schema/error.type';
import { Button, ButtonProps } from '@kit/ui/button';
import { toast } from '@kit/ui/sonner';
import { Trans } from '@kit/ui/trans';
import { cn } from '@kit/ui/utils';
export default function DoctorJobSelect({
className,
size = 'sm',
doctorUserId,
doctorName,
analysisOrderId,
userId,
isRemovable,
onJobUpdate,
linkTo,
}: {
className?: string;
size?: ButtonProps['size'];
doctorUserId?: string | null;
doctorName?: string;
analysisOrderId: number;
userId: string;
isRemovable?: boolean;
linkTo?: string;
onJobUpdate: () => void;
}) {
const [isPending, startTransition] = useTransition();
const router = useRouter();
const handleSelectJob = () => {
startTransition(async () => {
const result = await selectJobAction({
analysisOrderId,
userId,
});
if (result?.success) {
onJobUpdate();
linkTo && router.push(linkTo);
} else {
toast.error(
<Trans
i18nKey={`doctor:error.${result.reason ?? ErrorReason.UNKNOWN}`}
/>,
);
if (result.reason === ErrorReason.JOB_ASSIGNED) {
onJobUpdate();
}
}
});
};
const handleUnselectJob = () => {
startTransition(async () => {
const result = await unselectJobAction({
analysisOrderId,
});
if (result?.success) {
onJobUpdate();
} else {
toast.error(
<Trans
i18nKey={`doctor:error.${result.reason ?? ErrorReason.UNKNOWN}`}
/>,
);
}
});
};
if (isRemovable) {
return (
<Button
className={cn('w-16', className)}
size={size}
variant="destructive"
onClick={handleUnselectJob}
disabled={isPending}
>
{isPending ? (
<LoaderCircle className="h-4 w-4 animate-spin" />
) : (
<Trans i18nKey="doctor:unselectJob" />
)}
</Button>
);
}
if (!doctorUserId) {
return (
<Button
className={cn('w-16', className)}
size={size}
onClick={handleSelectJob}
disabled={isPending}
>
{isPending ? (
<LoaderCircle className="h-4 w-4 animate-spin" />
) : (
<Trans i18nKey="doctor:selectJob" />
)}
</Button>
);
}
return <>{doctorName}</>;
}

View File

@@ -6,6 +6,9 @@ import { usePathname } from 'next/navigation';
import { UserWorkspace } from '@/app/home/(user)/_lib/server/load-user-workspace'; import { UserWorkspace } from '@/app/home/(user)/_lib/server/load-user-workspace';
import { LayoutDashboard } from 'lucide-react'; import { LayoutDashboard } from 'lucide-react';
import { AppLogo } from '@kit/shared/components/app-logo';
import { ProfileAccountDropdownContainer } from '@kit/shared/components/personal-account-dropdown-container';
import { pathsConfig } from '@kit/shared/config';
import { import {
Sidebar, Sidebar,
SidebarContent, SidebarContent,
@@ -20,11 +23,6 @@ import {
} from '@kit/ui/shadcn-sidebar'; } from '@kit/ui/shadcn-sidebar';
import { Trans } from '@kit/ui/trans'; import { Trans } from '@kit/ui/trans';
import { AppLogo } from '@kit/shared/components/app-logo';
import { ProfileAccountDropdownContainer } from '@kit/shared/components/personal-account-dropdown-container';
import { pathsConfig } from '@kit/shared/config';
export function DoctorSidebar({ export function DoctorSidebar({
accounts, accounts,
}: { }: {
@@ -75,10 +73,7 @@ export function DoctorSidebar({
isActive={path === pathsConfig.app.myJobs} isActive={path === pathsConfig.app.myJobs}
asChild asChild
> >
<Link <Link className={'flex gap-2.5'} href={pathsConfig.app.myJobs}>
className={'flex gap-2.5'}
href={pathsConfig.app.myJobs}
>
<LayoutDashboard className={'h-4'} /> <LayoutDashboard className={'h-4'} />
<Trans i18nKey={'doctor:sidebar.myReviews'} /> <Trans i18nKey={'doctor:sidebar.myReviews'} />
</Link> </Link>

View File

@@ -3,15 +3,10 @@
import { useTransition } from 'react'; import { useTransition } from 'react';
import Link from 'next/link'; import Link from 'next/link';
import { useRouter } from 'next/navigation';
import { format } from 'date-fns'; import { format } from 'date-fns';
import { Eye, LoaderCircle } from 'lucide-react'; import { Eye } from 'lucide-react';
import {
selectJobAction,
unselectJobAction,
} from '@kit/doctor/actions/doctor-server-actions';
import { getResultSetName } from '@kit/doctor/lib/helpers'; import { getResultSetName } from '@kit/doctor/lib/helpers';
import { ResponseTable } from '@kit/doctor/schema/doctor-analysis.schema'; import { ResponseTable } from '@kit/doctor/schema/doctor-analysis.schema';
import { pathsConfig } from '@kit/shared/config'; import { pathsConfig } from '@kit/shared/config';
@@ -28,94 +23,7 @@ import {
TableRow, TableRow,
} from '@kit/ui/table'; } from '@kit/ui/table';
import { Trans } from '@kit/ui/trans'; import { Trans } from '@kit/ui/trans';
import DoctorJobSelect from './doctor-job-select';
function DoctorCell({
doctorUserId,
doctorName,
analysisOrderId,
userId,
isRemovable,
onJobUpdate,
linkTo,
}: {
doctorUserId?: string;
doctorName?: string;
analysisOrderId: number;
userId: string;
isRemovable?: boolean;
linkTo: string;
onJobUpdate: () => void;
}) {
const [isPending, startTransition] = useTransition();
const router = useRouter();
const handleSelectJob = () => {
startTransition(async () => {
const result = await selectJobAction({
analysisOrderId,
userId,
});
if (result?.success) {
onJobUpdate();
router.push(linkTo);
} else {
toast.error('common.genericServerError');
}
});
};
const handleUnselectJob = () => {
startTransition(async () => {
const result = await unselectJobAction({
analysisOrderId,
});
if (result?.success) {
onJobUpdate();
} else {
toast.error('common.genericServerError');
}
});
};
if (isRemovable) {
return (
<Button
className="w-16"
size="sm"
variant="destructive"
onClick={handleUnselectJob}
disabled={isPending}
>
{isPending ? (
<LoaderCircle className="h-4 w-4 animate-spin" />
) : (
<Trans i18nKey="doctor:unselectJob" />
)}
</Button>
);
}
if (!doctorUserId) {
return (
<Button
className="w-16"
size="sm"
onClick={handleSelectJob}
disabled={isPending}
>
{isPending ? (
<LoaderCircle className="h-4 w-4 animate-spin" />
) : (
<Trans i18nKey="doctor:selectJob" />
)}
</Button>
);
}
return <>{doctorName}</>;
}
export default function ResultsTable({ export default function ResultsTable({
results = [], results = [],
@@ -272,7 +180,7 @@ export default function ResultsTable({
/> />
</TableCell> </TableCell>
<TableCell> <TableCell>
<DoctorCell <DoctorJobSelect
doctorUserId={result.doctor?.primary_owner_user_id} doctorUserId={result.doctor?.primary_owner_user_id}
doctorName={getFullName( doctorName={getFullName(
result.doctor?.name, result.doctor?.name,

View File

@@ -2,6 +2,12 @@ import { cache } from 'react';
import { getAnalysisResultsForDoctor } from '@kit/doctor/services/doctor-analysis.service'; import { getAnalysisResultsForDoctor } from '@kit/doctor/services/doctor-analysis.service';
import { PageBody, PageHeader } from '@kit/ui/page'; import { PageBody, PageHeader } from '@kit/ui/page';
import {
DoctorPageViewAction,
createDoctorPageViewLog,
} from '~/lib/services/audit/doctorPageView.service';
import AnalysisView from '../../_components/analysis-view'; import AnalysisView from '../../_components/analysis-view';
import { DoctorGuard } from '../../_components/doctor-guard'; import { DoctorGuard } from '../../_components/doctor-guard';
@@ -19,6 +25,14 @@ async function AnalysisPage({
return null; return null;
} }
if (analysisResultDetails) {
await createDoctorPageViewLog({
action: DoctorPageViewAction.VIEW_ANALYSIS_RESULTS,
recordKey: id,
dataOwnerUserId: analysisResultDetails.patient.userId,
});
}
return ( return (
<> <>
<PageHeader /> <PageHeader />

View File

@@ -1,10 +1,19 @@
import { getUserDoneResponsesAction } from '@kit/doctor/actions/table-data-fetching-actions';
import { PageBody, PageHeader } from '@kit/ui/page'; import { PageBody, PageHeader } from '@kit/ui/page';
import { getUserDoneResponsesAction } from '@kit/doctor/actions/table-data-fetching-actions'; import {
import ResultsTableWrapper from '../_components/results-table-wrapper'; DoctorPageViewAction,
createDoctorPageViewLog,
} from '~/lib/services/audit/doctorPageView.service';
import { DoctorGuard } from '../_components/doctor-guard'; import { DoctorGuard } from '../_components/doctor-guard';
import ResultsTableWrapper from '../_components/results-table-wrapper';
async function CompletedJobsPage() { async function CompletedJobsPage() {
await createDoctorPageViewLog({
action: DoctorPageViewAction.VIEW_DONE_JOBS,
});
return ( return (
<> <>
<PageHeader /> <PageHeader />

View File

@@ -1,9 +1,19 @@
import { getUserInProgressResponsesAction } from '@kit/doctor/actions/table-data-fetching-actions'; import { getUserInProgressResponsesAction } from '@kit/doctor/actions/table-data-fetching-actions';
import { PageBody, PageHeader } from '@kit/ui/page'; import { PageBody, PageHeader } from '@kit/ui/page';
import ResultsTableWrapper from '../_components/results-table-wrapper';
import {
DoctorPageViewAction,
createDoctorPageViewLog,
} from '~/lib/services/audit/doctorPageView.service';
import { DoctorGuard } from '../_components/doctor-guard'; import { DoctorGuard } from '../_components/doctor-guard';
import ResultsTableWrapper from '../_components/results-table-wrapper';
async function MyReviewsPage() { async function MyReviewsPage() {
await createDoctorPageViewLog({
action: DoctorPageViewAction.VIEW_OWN_JOBS,
});
return ( return (
<> <>
<PageHeader /> <PageHeader />

View File

@@ -1,10 +1,19 @@
import { getOpenResponsesAction } from '@kit/doctor/actions/table-data-fetching-actions'; import { getOpenResponsesAction } from '@kit/doctor/actions/table-data-fetching-actions';
import { PageBody, PageHeader } from '@kit/ui/page'; import { PageBody, PageHeader } from '@kit/ui/page';
import {
DoctorPageViewAction,
createDoctorPageViewLog,
} from '~/lib/services/audit/doctorPageView.service';
import { DoctorGuard } from '../_components/doctor-guard'; import { DoctorGuard } from '../_components/doctor-guard';
import ResultsTableWrapper from '../_components/results-table-wrapper'; import ResultsTableWrapper from '../_components/results-table-wrapper';
async function OpenJobsPage() { async function OpenJobsPage() {
await createDoctorPageViewLog({
action: DoctorPageViewAction.VIEW_OPEN_JOBS,
});
return ( return (
<> <>
<PageHeader /> <PageHeader />

View File

@@ -1,8 +1,18 @@
import { PageBody, PageHeader } from '@kit/ui/page'; import { PageBody, PageHeader } from '@kit/ui/page';
import {
DoctorPageViewAction,
createDoctorPageViewLog,
} from '~/lib/services/audit/doctorPageView.service';
import Dashboard from './_components/doctor-dashboard'; import Dashboard from './_components/doctor-dashboard';
import { DoctorGuard } from './_components/doctor-guard'; import { DoctorGuard } from './_components/doctor-guard';
async function DoctorPage() { async function DoctorPage() {
await createDoctorPageViewLog({
action: DoctorPageViewAction.VIEW_DASHBOARD,
});
return ( return (
<> <>
<PageHeader /> <PageHeader />

View File

@@ -1,6 +1,6 @@
'use client'; 'use client';
import React, { useMemo, useState } from 'react'; import React, { ReactElement, ReactNode, useMemo, useState } from 'react';
import { UserAnalysisElement } from '@/packages/features/accounts/src/types/accounts'; import { UserAnalysisElement } from '@/packages/features/accounts/src/types/accounts';
import { format } from 'date-fns'; import { format } from 'date-fns';
@@ -37,11 +37,15 @@ export enum AnalysisStatus {
const Analysis = ({ const Analysis = ({
analysisElement, analysisElement,
results, results,
startIcon,
endIcon,
isCancelled, isCancelled,
}: { }: {
analysisElement: AnalysisElement; analysisElement: Pick<AnalysisElement, 'analysis_name_lab'>;
results?: UserAnalysisElement; results?: AnalysisResultForDisplay;
isCancelled?: boolean; isCancelled?: boolean;
startIcon?: ReactElement | null;
endIcon?: ReactNode | null;
}) => { }) => {
const name = analysisElement.analysis_name_lab || ''; const name = analysisElement.analysis_name_lab || '';
const status = results?.norm_status || AnalysisStatus.NORMAL; const status = results?.norm_status || AnalysisStatus.NORMAL;
@@ -78,59 +82,66 @@ const Analysis = ({
}, [results, value, normLower]); }, [results, value, normLower]);
return ( return (
<div className="border-border flex flex-col items-center justify-between gap-2 rounded-lg border px-5 px-12 py-3 sm:h-[65px] sm:flex-row sm:gap-0"> <div className="border-border rounded-lg border px-5">
<div className="flex items-center gap-2 font-semibold"> <div className="flex flex-col items-center justify-between gap-2 py-3 sm:h-[65px] sm:flex-row sm:gap-0">
{name} <div className="flex items-center gap-2 font-semibold">
{results?.response_time && ( {startIcon || <div className="w-4" />}
<div {name}
className="group/tooltip relative" {results?.response_time && (
onClick={() => setShowTooltip(!showTooltip)}
onMouseLeave={() => setShowTooltip(false)}
>
<Info className="hover" />{' '}
<div <div
className={cn( className="group/tooltip relative"
'absolute bottom-full left-1/2 z-10 mb-2 hidden -translate-x-1/2 rounded border bg-white p-4 text-sm whitespace-nowrap group-hover/tooltip:block', onClick={(e) => {
{ block: showTooltip }, e.stopPropagation();
)} setShowTooltip(!showTooltip);
}}
onMouseLeave={() => setShowTooltip(false)}
> >
<Trans i18nKey="analysis-results:analysisDate" /> <Info className="hover" />{' '}
{': '} <div
{format(new Date(results.response_time), 'dd.MM.yyyy HH:mm')} className={cn(
'absolute bottom-full left-1/2 z-10 mb-2 hidden -translate-x-1/2 rounded border bg-white p-4 text-sm whitespace-nowrap group-hover/tooltip:block',
{ block: showTooltip },
)}
>
<Trans i18nKey="analysis-results:analysisDate" />
{': '}
{format(new Date(results.response_time), 'dd.MM.yyyy HH:mm')}
</div>
</div> </div>
</div> )}
)} </div>
{results ? (
<>
<div className="flex items-center gap-3 sm:ml-auto">
<div className="font-semibold">{value}</div>
<div className="text-muted-foreground text-sm">{unit}</div>
</div>
<div className="text-muted-foreground mx-8 flex flex-col-reverse gap-2 text-center text-sm sm:block sm:gap-0">
{normLower} - {normUpper}
<div>
<Trans i18nKey="analysis-results:results.range.normal" />
</div>
</div>
<AnalysisLevelBar
results={results}
normLowerIncluded={normLowerIncluded}
normUpperIncluded={normUpperIncluded}
level={analysisResultLevel!}
/>
{endIcon || <div className="mx-2 w-4" />}
</>
) : (isCancelled ? null : (
<>
<div className="flex items-center gap-3 sm:ml-auto">
<div className="font-semibold">
<Trans i18nKey="analysis-results:waitingForResults" />
</div>
</div>
<div className="mx-8 w-[60px]"></div>
<AnalysisLevelBarSkeleton />
</>
))}
</div> </div>
{results ? (
<>
<div className="flex items-center gap-3 sm:ml-auto">
<div className="font-semibold">{value}</div>
<div className="text-muted-foreground text-sm">{unit}</div>
</div>
<div className="text-muted-foreground mx-8 flex flex-col-reverse gap-2 text-center text-sm sm:block sm:gap-0">
{normLower} - {normUpper} %
<div>
<Trans i18nKey="analysis-results:results.range.normal" />
</div>
</div>
<AnalysisLevelBar
results={results}
normLowerIncluded={normLowerIncluded}
normUpperIncluded={normUpperIncluded}
level={analysisResultLevel!}
/>
</>
) : (isCancelled ? null : (
<>
<div className="flex items-center gap-3 sm:ml-auto">
<div className="font-semibold">
<Trans i18nKey="analysis-results:waitingForResults" />
</div>
</div>
<div className="mx-8 w-[60px]"></div>
<AnalysisLevelBarSkeleton />
</>
))}
</div> </div>
); );
}; };

View File

@@ -13,7 +13,7 @@ import { pathsConfig } from '@kit/shared/config';
import { getAnalysisElements } from '~/lib/services/analysis-element.service'; import { getAnalysisElements } from '~/lib/services/analysis-element.service';
import { import {
PAGE_VIEW_ACTION, PageViewAction,
createPageViewLog, createPageViewLog,
} from '~/lib/services/audit/pageView.service'; } from '~/lib/services/audit/pageView.service';
import { AnalysisOrder, getAnalysisOrders } from '~/lib/services/order.service'; import { AnalysisOrder, getAnalysisOrders } from '~/lib/services/order.service';
@@ -50,7 +50,7 @@ async function AnalysisResultsPage() {
await createPageViewLog({ await createPageViewLog({
accountId: account.id, accountId: account.id,
action: PAGE_VIEW_ACTION.VIEW_ANALYSIS_RESULTS, action: PageViewAction.VIEW_ANALYSIS_RESULTS,
}); });
const getAnalysisElementIds = (analysisOrders: AnalysisOrder[]) => [ const getAnalysisElementIds = (analysisOrders: AnalysisOrder[]) => [

View File

@@ -8,7 +8,10 @@ import { z } from 'zod';
import { UserWorkspaceContextProvider } from '@kit/accounts/components'; import { UserWorkspaceContextProvider } from '@kit/accounts/components';
import { AppLogo } from '@kit/shared/components/app-logo'; import { AppLogo } from '@kit/shared/components/app-logo';
import { personalAccountNavigationConfig } from '@kit/shared/config'; import {
pathsConfig,
personalAccountNavigationConfig,
} from '@kit/shared/config';
import { Page, PageMobileNavigation, PageNavigation } from '@kit/ui/page'; import { Page, PageMobileNavigation, PageNavigation } from '@kit/ui/page';
import { SidebarProvider } from '@kit/ui/shadcn-sidebar'; import { SidebarProvider } from '@kit/ui/shadcn-sidebar';
@@ -92,7 +95,7 @@ function MobileNavigation({
}) { }) {
return ( return (
<> <>
<AppLogo /> <AppLogo href={pathsConfig.app.home} />
<HomeMobileNavigation workspace={workspace} cart={cart} /> <HomeMobileNavigation workspace={workspace} cart={cart} />
</> </>

View File

@@ -6,7 +6,7 @@ import { Trans } from '@kit/ui/trans';
import { HomeLayoutPageHeader } from '../../_components/home-page-header'; import { HomeLayoutPageHeader } from '../../_components/home-page-header';
import { loadAnalyses } from '../../_lib/server/load-analyses'; import { loadAnalyses } from '../../_lib/server/load-analyses';
import OrderAnalysesCards from '../../_components/order-analyses-cards'; import OrderAnalysesCards from '../../_components/order-analyses-cards';
import { createPageViewLog, PAGE_VIEW_ACTION } from '~/lib/services/audit/pageView.service'; import { createPageViewLog, PageViewAction } from '~/lib/services/audit/pageView.service';
import { loadCurrentUserAccount } from '../../_lib/server/load-user-account'; import { loadCurrentUserAccount } from '../../_lib/server/load-user-account';
export const generateMetadata = async () => { export const generateMetadata = async () => {
@@ -27,7 +27,7 @@ async function OrderAnalysisPage() {
await createPageViewLog({ await createPageViewLog({
accountId: account.id, accountId: account.id,
action: PAGE_VIEW_ACTION.VIEW_ORDER_ANALYSIS, action: PageViewAction.VIEW_ORDER_ANALYSIS,
}); });
return ( return (

View File

@@ -1,27 +1,35 @@
import Link from 'next/link'; import Link from 'next/link';
import { createI18nServerInstance } from '@/lib/i18n/i18n.server';
import { pathsConfig } from '@/packages/shared/src/config';
import { formatCurrency } from '@/packages/shared/src/utils';
import { SIDEBAR_WIDTH_PROPERTY } from '@/packages/ui/src/shadcn/constants';
import { StoreCart } from '@medusajs/types';
import { ShoppingCart } from 'lucide-react'; import { ShoppingCart } from 'lucide-react';
import { Trans } from '@kit/ui/trans';
import { AppLogo } from '@kit/shared/components/app-logo'; import { AppLogo } from '@kit/shared/components/app-logo';
import { ProfileAccountDropdownContainer } from '@kit/shared/components/personal-account-dropdown-container'; import { ProfileAccountDropdownContainer } from '@kit/shared/components/personal-account-dropdown-container';
import { Search } from '@kit/shared/components/ui/search'; import { Search } from '@kit/shared/components/ui/search';
import { SIDEBAR_WIDTH_PROPERTY } from '@/packages/ui/src/shadcn/constants';
import { Button } from '@kit/ui/button'; import { Button } from '@kit/ui/button';
import { Card } from '@kit/ui/shadcn/card';
import { Trans } from '@kit/ui/trans';
import { UserNotifications } from '../_components/user-notifications'; import { UserNotifications } from '../_components/user-notifications';
import { type UserWorkspace } from '../_lib/server/load-user-workspace'; import { type UserWorkspace } from '../_lib/server/load-user-workspace';
import { StoreCart } from '@medusajs/types';
import { formatCurrency } from '@/packages/shared/src/utils';
import { createI18nServerInstance } from '@/lib/i18n/i18n.server';
export async function HomeMenuNavigation(props: { workspace: UserWorkspace, cart: StoreCart | null }) { export async function HomeMenuNavigation(props: {
workspace: UserWorkspace;
cart: StoreCart | null;
}) {
const { language } = await createI18nServerInstance(); const { language } = await createI18nServerInstance();
const { workspace, user, accounts } = props.workspace; const { workspace, user, accounts } = props.workspace;
const totalValue = props.cart?.total ? formatCurrency({ const totalValue = props.cart?.total
currencyCode: props.cart.currency_code, ? formatCurrency({
locale: language, currencyCode: props.cart.currency_code,
value: props.cart.total, locale: language,
}) : 0; value: props.cart.total,
})
: 0;
const cartItemsCount = props.cart?.items?.length ?? 0; const cartItemsCount = props.cart?.items?.length ?? 0;
const hasCartItems = cartItemsCount > 0; const hasCartItems = cartItemsCount > 0;
@@ -29,8 +37,7 @@ export async function HomeMenuNavigation(props: { workspace: UserWorkspace, cart
return ( return (
<div className={'flex w-full flex-1 items-center justify-between gap-3'}> <div className={'flex w-full flex-1 items-center justify-between gap-3'}>
<div className={`flex items-center ${SIDEBAR_WIDTH_PROPERTY}`}> <div className={`flex items-center ${SIDEBAR_WIDTH_PROPERTY}`}>
<AppLogo /> <AppLogo href={pathsConfig.app.home} />
</div> </div>
<Search <Search
className="flex grow" className="flex grow"
@@ -38,15 +45,27 @@ export async function HomeMenuNavigation(props: { workspace: UserWorkspace, cart
/> />
<div className="flex items-center justify-end gap-3"> <div className="flex items-center justify-end gap-3">
<Card className="px-6 py-2">
<span> {Number(0).toFixed(2).replace('.', ',')}</span>
</Card>
{hasCartItems && ( {hasCartItems && (
<Button className='relative px-4 py-2 h-10 border-1 mr-0 cursor-pointer' variant='ghost'> <Button
<span className='flex items-center text-nowrap'>{totalValue}</span> className="relative mr-0 h-10 cursor-pointer border-1 px-4 py-2"
variant="ghost"
>
<span className="flex items-center text-nowrap">{totalValue}</span>
</Button> </Button>
)} )}
<Link href='/home/cart'> <Link href="/home/cart">
<Button variant="ghost" className='relative px-4 py-2 h-10 border-1 mr-0 cursor-pointer' > <Button
variant="ghost"
className="relative mr-0 h-10 cursor-pointer border-1 px-4 py-2"
>
<ShoppingCart className="stroke-[1.5px]" /> <ShoppingCart className="stroke-[1.5px]" />
<Trans i18nKey="common:shoppingCartCount" values={{ count: cartItemsCount }} /> <Trans
i18nKey="common:shoppingCartCount"
values={{ count: cartItemsCount }}
/>
</Button> </Button>
</Link> </Link>
<UserNotifications userId={user.id} /> <UserNotifications userId={user.id} />

View File

@@ -1,29 +1,40 @@
import React, { use } from 'react'; import React from 'react';
import { redirect } from 'next/navigation'; import { redirect } from 'next/navigation';
import { formatCurrency } from '@/packages/shared/src/utils'; import { formatCurrency } from '@/packages/shared/src/utils';
import { Database } from '@/packages/supabase/src/database.types';
import { PiggyBankIcon, Settings } from 'lucide-react'; import { PiggyBankIcon, Settings } from 'lucide-react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { createPath, pathsConfig } from '@kit/shared/config';
import { Card, CardTitle } from '@kit/ui/card'; import { Card, CardTitle } from '@kit/ui/card';
import { cn } from '@kit/ui/lib/utils';
import { Button } from '@kit/ui/shadcn/button'; import { Button } from '@kit/ui/shadcn/button';
import { Trans } from '@kit/ui/trans'; import { Trans } from '@kit/ui/trans';
import { pathsConfig } from '@kit/shared/config';
import { createPath } from '@kit/shared/config/team-account-navigation.config';
interface TeamAccountBenefitStatisticsProps { interface TeamAccountBenefitStatisticsProps {
employeeCount: number;
accountSlug: string; accountSlug: string;
companyParams: Database['medreport']['Tables']['company_params']['Row'];
} }
const StatisticsCard = ({ children }: { children: React.ReactNode }) => { const StatisticsCard = ({ children }: { children: React.ReactNode }) => {
return <Card className="p-4">{children}</Card>; return <Card className="p-4">{children}</Card>;
}; };
const StatisticsCardTitle = ({ children }: { children: React.ReactNode }) => { const StatisticsCardTitle = ({
return <CardTitle className="text-sm font-medium">{children}</CardTitle>; children,
className,
}: {
children: React.ReactNode;
className?: string;
}) => {
return (
<CardTitle className={cn('text-sm font-medium', className)}>
{children}
</CardTitle>
);
}; };
const StatisticsDescription = ({ children }: { children: React.ReactNode }) => { const StatisticsDescription = ({ children }: { children: React.ReactNode }) => {
@@ -35,7 +46,9 @@ const StatisticsValue = ({ children }: { children: React.ReactNode }) => {
}; };
const TeamAccountBenefitStatistics = ({ const TeamAccountBenefitStatistics = ({
employeeCount,
accountSlug, accountSlug,
companyParams,
}: TeamAccountBenefitStatisticsProps) => { }: TeamAccountBenefitStatisticsProps) => {
const { const {
i18n: { language }, i18n: { language },
@@ -77,7 +90,8 @@ const TeamAccountBenefitStatistics = ({
i18nKey="teams:benefitStatistics.budget.volume" i18nKey="teams:benefitStatistics.budget.volume"
values={{ values={{
volume: formatCurrency({ volume: formatCurrency({
value: 15000, value:
(Number(companyParams.benefit_amount) || 0) * employeeCount,
locale: language, locale: language,
currencyCode: 'EUR', currencyCode: 'EUR',
}), }),
@@ -88,11 +102,17 @@ const TeamAccountBenefitStatistics = ({
</Card> </Card>
<div className="grid flex-2 grid-cols-2 gap-2 sm:grid-cols-3 sm:grid-rows-2"> <div className="grid flex-2 grid-cols-2 gap-2 sm:grid-cols-3 sm:grid-rows-2">
<StatisticsCard>
<StatisticsCardTitle className="text-lg font-bold">
<Trans i18nKey="teams:benefitStatistics.data.serviceSum" />
</StatisticsCardTitle>
<StatisticsValue>1800 </StatisticsValue>
</StatisticsCard>
<StatisticsCard> <StatisticsCard>
<StatisticsCardTitle> <StatisticsCardTitle>
<Trans i18nKey="teams:benefitStatistics.data.analysis" /> <Trans i18nKey="teams:benefitStatistics.data.analysis" />
</StatisticsCardTitle> </StatisticsCardTitle>
<StatisticsValue>18 %</StatisticsValue> <StatisticsValue>200 </StatisticsValue>
<StatisticsDescription> <StatisticsDescription>
<Trans <Trans
i18nKey="teams:benefitStatistics.data.reservations" i18nKey="teams:benefitStatistics.data.reservations"
@@ -104,7 +124,7 @@ const TeamAccountBenefitStatistics = ({
<StatisticsCardTitle> <StatisticsCardTitle>
<Trans i18nKey="teams:benefitStatistics.data.doctorsAndSpecialists" /> <Trans i18nKey="teams:benefitStatistics.data.doctorsAndSpecialists" />
</StatisticsCardTitle> </StatisticsCardTitle>
<StatisticsValue>22 %</StatisticsValue> <StatisticsValue>200 </StatisticsValue>
<StatisticsDescription> <StatisticsDescription>
<Trans <Trans
i18nKey="teams:benefitStatistics.data.reservations" i18nKey="teams:benefitStatistics.data.reservations"
@@ -116,7 +136,7 @@ const TeamAccountBenefitStatistics = ({
<StatisticsCardTitle> <StatisticsCardTitle>
<Trans i18nKey="teams:benefitStatistics.data.researches" /> <Trans i18nKey="teams:benefitStatistics.data.researches" />
</StatisticsCardTitle> </StatisticsCardTitle>
<StatisticsValue>20 %</StatisticsValue> <StatisticsValue>200 </StatisticsValue>
<StatisticsDescription> <StatisticsDescription>
<Trans <Trans
i18nKey="teams:benefitStatistics.data.reservations" i18nKey="teams:benefitStatistics.data.reservations"
@@ -125,8 +145,10 @@ const TeamAccountBenefitStatistics = ({
</StatisticsDescription> </StatisticsDescription>
</StatisticsCard> </StatisticsCard>
<StatisticsCard> <StatisticsCard>
<StatisticsCardTitle>E-konsultatsioon</StatisticsCardTitle> <StatisticsCardTitle>
<StatisticsValue>17 %</StatisticsValue> <Trans i18nKey="teams:benefitStatistics.data.eclinic" />
</StatisticsCardTitle>
<StatisticsValue>200 </StatisticsValue>
<StatisticsDescription> <StatisticsDescription>
<Trans <Trans
i18nKey="teams:benefitStatistics.data.reservations" i18nKey="teams:benefitStatistics.data.reservations"
@@ -134,28 +156,19 @@ const TeamAccountBenefitStatistics = ({
/> />
</StatisticsDescription> </StatisticsDescription>
</StatisticsCard> </StatisticsCard>
<Card className="col-span-2 p-4">
<StatisticsCard>
<StatisticsCardTitle> <StatisticsCardTitle>
<Trans i18nKey="teams:benefitStatistics.data.healthResearchPlans" /> <Trans i18nKey="teams:benefitStatistics.data.healthResearchPlans" />
</StatisticsCardTitle> </StatisticsCardTitle>
<div className="grid grid-cols-2 gap-2"> <StatisticsValue>200 </StatisticsValue>
<div className="border-r"> <StatisticsDescription>
<StatisticsValue>23 %</StatisticsValue> <Trans
<StatisticsDescription> i18nKey="teams:benefitStatistics.data.serviceUsage"
<Trans values={{ value: 46 }}
i18nKey="teams:benefitStatistics.data.serviceUsage" />
values={{ value: 46 }} </StatisticsDescription>
/> </StatisticsCard>
</StatisticsDescription>
</div>
<div className="ml-4">
<StatisticsValue>1800 </StatisticsValue>
<StatisticsDescription>
<Trans i18nKey="teams:benefitStatistics.data.serviceSum" />
</StatisticsDescription>
</div>
</div>
</Card>
</div> </div>
</div> </div>
); );

View File

@@ -1,33 +1,36 @@
import React from 'react'; import React from 'react';
import { Database } from '@/packages/supabase/src/database.types';
import { Card } from '@kit/ui/card'; import { Card } from '@kit/ui/card';
import { Trans } from '@kit/ui/trans'; import { Trans } from '@kit/ui/trans';
import { cn } from '@kit/ui/utils'; import { cn } from '@kit/ui/utils';
import { import { getAccountHealthDetailsFields } from '../_lib/server/load-team-account-health-details';
NormStatus,
getAccountHealthDetailsFields,
} from '../_lib/server/load-team-account-health-details';
import { TeamAccountStatisticsProps } from './team-account-statistics'; import { TeamAccountStatisticsProps } from './team-account-statistics';
const TeamAccountHealthDetails = ({ const TeamAccountHealthDetails = ({
memberParams, memberParams,
bmiThresholds,
members,
}: { }: {
memberParams: TeamAccountStatisticsProps['memberParams']; memberParams: TeamAccountStatisticsProps['memberParams'];
bmiThresholds: Omit<
Database['medreport']['Tables']['bmi_thresholds']['Row'],
'id'
>[];
members: Database['medreport']['Functions']['get_account_members']['Returns'];
}) => { }) => {
const accountHealthDetailsFields = const accountHealthDetailsFields = getAccountHealthDetailsFields(
getAccountHealthDetailsFields(memberParams); memberParams,
bmiThresholds,
members,
);
return ( return (
<div className="grid flex-1 grid-cols-2 gap-4 md:grid-cols-3"> <div className="grid flex-1 grid-cols-2 gap-4 md:grid-cols-3">
{accountHealthDetailsFields.map(({ title, Icon, value, normStatus }) => ( {accountHealthDetailsFields.map(({ title, Icon, value, iconBg }) => (
<Card className="relative p-4" key={title}> <Card className="relative p-4" key={title}>
<div <div className={cn('absolute top-2 right-2 rounded-2xl p-2', iconBg)}>
className={cn('absolute top-2 right-2 rounded-2xl p-2', {
'bg-success': normStatus === NormStatus.NORMAL,
'bg-warning': normStatus === NormStatus.WARNING,
'bg-destructive': normStatus === NormStatus.CRITICAL,
})}
>
<Icon color="white" className="stroke-2" /> <Icon color="white" className="stroke-2" />
</div> </div>
<h5 className="mt-8 leading-none"> <h5 className="mt-8 leading-none">

View File

@@ -1,6 +1,8 @@
import type { User } from '@supabase/supabase-js'; import type { User } from '@supabase/supabase-js';
import { ApplicationRole } from '@kit/accounts/types/accounts'; import { ApplicationRole } from '@kit/accounts/types/accounts';
import { ProfileAccountDropdownContainer } from '@kit/shared/components/personal-account-dropdown-container';
import { getTeamAccountSidebarConfig } from '@kit/shared/config';
import { import {
Sidebar, Sidebar,
SidebarContent, SidebarContent,
@@ -8,8 +10,6 @@ import {
SidebarHeader, SidebarHeader,
} from '@kit/ui/shadcn-sidebar'; } from '@kit/ui/shadcn-sidebar';
import { ProfileAccountDropdownContainer } from '@kit/shared/components//personal-account-dropdown-container';
import { getTeamAccountSidebarConfig } from '@kit/shared/config/team-account-navigation.config';
import { TeamAccountNotifications } from '~/home/[account]/_components/team-account-notifications'; import { TeamAccountNotifications } from '~/home/[account]/_components/team-account-notifications';
import { TeamAccountAccountsSelector } from '../_components/team-account-accounts-selector'; import { TeamAccountAccountsSelector } from '../_components/team-account-accounts-selector';

View File

@@ -1,8 +1,12 @@
import { useMemo } from 'react'; import { useMemo } from 'react';
import {
getTeamAccountSidebarConfig,
pathsConfig,
} from '@/packages/shared/src/config';
import { AppLogo } from '@kit/shared/components/app-logo'; import { AppLogo } from '@kit/shared/components/app-logo';
import { ProfileAccountDropdownContainer } from '@kit/shared/components/personal-account-dropdown-container'; import { ProfileAccountDropdownContainer } from '@kit/shared/components/personal-account-dropdown-container';
import { getTeamAccountSidebarConfig } from '@kit/shared/config/team-account-navigation.config';
// local imports // local imports
import { TeamAccountWorkspace } from '../_lib/server/team-account-workspace.loader'; import { TeamAccountWorkspace } from '../_lib/server/team-account-workspace.loader';
@@ -46,7 +50,7 @@ export function TeamAccountNavigationMenu(props: {
return ( return (
<div className={'flex w-full flex-1 justify-between'}> <div className={'flex w-full flex-1 justify-between'}>
<div className={'flex items-center space-x-8'}> <div className={'flex items-center space-x-8'}>
<AppLogo /> <AppLogo href={pathsConfig.app.home} />
</div> </div>
<div className={'flex items-center justify-end gap-2 space-x-2.5'}> <div className={'flex items-center justify-end gap-2 space-x-2.5'}>

View File

@@ -1,16 +1,25 @@
'use client'; 'use client';
import { useState } from 'react';
import { redirect } from 'next/navigation'; import { redirect } from 'next/navigation';
import { Database } from '@/packages/supabase/src/database.types'; import { Database } from '@/packages/supabase/src/database.types';
import { ChevronRight, Euro, User } from 'lucide-react'; import { format } from 'date-fns';
import { enGB, et } from 'date-fns/locale';
import { CalendarIcon, ChevronRight, Euro, User } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { createPath, pathsConfig } from '@kit/shared/config';
import { Card } from '@kit/ui/card'; import { Card } from '@kit/ui/card';
import { Trans } from '@kit/ui/makerkit/trans'; import { Trans } from '@kit/ui/makerkit/trans';
import { Button } from '@kit/ui/shadcn/button';
import { pathsConfig } from '@kit/shared/config'; import { Calendar, DateRange } from '@kit/ui/shadcn/calendar';
import {
import { createPath } from '@kit/shared/config/team-account-navigation.config'; Popover,
PopoverContent,
PopoverTrigger,
} from '@kit/ui/shadcn/popover';
import TeamAccountBenefitStatistics from './team-account-benefit-statistics'; import TeamAccountBenefitStatistics from './team-account-benefit-statistics';
import TeamAccountHealthDetails from './team-account-health-details'; import TeamAccountHealthDetails from './team-account-health-details';
@@ -21,82 +30,145 @@ export interface TeamAccountStatisticsProps {
Database['medreport']['Tables']['account_params']['Row'], Database['medreport']['Tables']['account_params']['Row'],
'weight' | 'height' 'weight' | 'height'
>[]; >[];
bmiThresholds: Omit<
Database['medreport']['Tables']['bmi_thresholds']['Row'],
'id'
>[];
members: Database['medreport']['Functions']['get_account_members']['Returns'];
companyParams: Database['medreport']['Tables']['company_params']['Row'];
} }
export default function TeamAccountStatistics({ export default function TeamAccountStatistics({
teamAccount, teamAccount,
memberParams, memberParams,
bmiThresholds,
members,
companyParams,
}: TeamAccountStatisticsProps) { }: TeamAccountStatisticsProps) {
const [date, setDate] = useState<DateRange | undefined>({
from: new Date(),
to: new Date(),
});
const {
i18n: { language },
} = useTranslation();
const dateFormatOptions = {
locale: language === 'et' ? et : enGB,
};
return ( return (
<div <>
className={ <div className="mt-4 flex items-center justify-between">
'animate-in fade-in flex flex-col space-y-4 pb-36 duration-500' <h4 className="font-bold">
} <Trans
> i18nKey={'teams:home.headerTitle'}
<TeamAccountBenefitStatistics accountSlug={teamAccount.slug || ''} /> values={{ companyName: teamAccount.name }}
/>
</h4>
<h5 className="mt-4 mb-2"> <Popover>
<Trans i18nKey="teams:home.healthDetails" /> <PopoverTrigger asChild>
</h5> <Button variant="outline" data-empty={!date}>
<div className="flex flex-col gap-2 sm:flex-row"> <CalendarIcon />
<TeamAccountHealthDetails memberParams={memberParams} /> {date?.from && date?.to ? (
`${format(date.from, 'd MMMM yyyy', dateFormatOptions)} - ${format(date.to, 'd MMMM yyyy', dateFormatOptions)}`
) : (
<span>
<Trans i18nKey="common:formField.selectDate" />
</span>
)}
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0">
<Calendar
mode="range"
selected={date}
onSelect={setDate}
locale={language === 'et' ? et : enGB}
/>
</PopoverContent>
</Popover>
</div>
<div className="flex flex-col gap-2"> <div
<Card className={
variant="gradient-success" 'animate-in fade-in flex flex-col space-y-4 pb-36 duration-500'
className="border-success/50 hover:bg-success/20 relative flex h-full cursor-pointer flex-col justify-center px-6 py-4 transition-colors" }
onClick={() => >
redirect( <TeamAccountBenefitStatistics
createPath( employeeCount={members.length}
pathsConfig.app.accountMembers, accountSlug={teamAccount.slug || ''}
teamAccount.slug || '', companyParams={companyParams}
), />
)
} <h5 className="mt-4 mb-2">
> <Trans i18nKey="teams:home.healthDetails" />
<div className="flex flex-col"> </h5>
<div className="flex flex-col gap-2 sm:flex-row">
<TeamAccountHealthDetails
memberParams={memberParams}
bmiThresholds={bmiThresholds}
members={members}
/>
<div className="flex flex-col gap-2">
<Card
variant="gradient-success"
className="border-success/50 hover:bg-success/20 relative flex h-full cursor-pointer flex-col justify-center px-6 py-4 transition-colors"
onClick={() =>
redirect(
createPath(
pathsConfig.app.accountMembers,
teamAccount.slug || '',
),
)
}
>
<div className="flex flex-col">
<div className="border-input absolute top-2 right-2 rounded-md border bg-white p-3">
<ChevronRight className="stroke-2" />
</div>
<div className="bg-primary/10 w-fit rounded-2xl p-2">
<User color="green" className="stroke-2" />
</div>
<span className="mt-4 mb-2 text-lg font-semibold">
<Trans i18nKey="teams:home.membersSettingsButtonTitle" />
</span>
<p className="text-muted-foreground text-sm">
<Trans i18nKey="teams:home.membersSettingsButtonDescription" />
</p>
</div>
</Card>
<Card
variant="gradient-warning"
className="border-warning/40 hover:bg-warning/20 relative flex h-full cursor-pointer flex-col justify-center px-6 py-4 transition-colors"
onClick={() =>
redirect(
createPath(
pathsConfig.app.accountBilling,
teamAccount.slug || '',
),
)
}
>
<div className="border-input absolute top-2 right-2 rounded-md border bg-white p-3"> <div className="border-input absolute top-2 right-2 rounded-md border bg-white p-3">
<ChevronRight className="stroke-2" /> <ChevronRight className="stroke-2" />
</div> </div>
<div className="bg-primary/10 w-fit rounded-2xl p-2"> <div className="bg-warning/10 w-fit rounded-2xl p-2">
<User color="green" className="stroke-2" /> <Euro color="orange" className="stroke-2" />
</div> </div>
<span className="mt-4 mb-2 text-lg font-semibold"> <span className="mt-4 mb-2 text-lg font-semibold">
<Trans i18nKey="teams:home.membersSettingsButtonTitle" /> <Trans i18nKey="teams:home.membersBillingButtonTitle" />
</span> </span>
<p className="text-muted-foreground text-sm"> <p className="text-muted-foreground text-sm">
<Trans i18nKey="teams:home.membersSettingsButtonDescription" /> <Trans i18nKey="teams:home.membersBillingButtonDescription" />
</p> </p>
</div> </Card>
</Card> </div>
<Card
variant="gradient-warning"
className="border-warning/40 hover:bg-warning/20 relative flex h-full cursor-pointer flex-col justify-center px-6 py-4 transition-colors"
onClick={() =>
redirect(
createPath(
pathsConfig.app.accountBilling,
teamAccount.slug || '',
),
)
}
>
<div className="border-input absolute top-2 right-2 rounded-md border bg-white p-3">
<ChevronRight className="stroke-2" />
</div>
<div className="bg-warning/10 w-fit rounded-2xl p-2">
<Euro color="orange" className="stroke-2" />
</div>
<span className="mt-4 mb-2 text-lg font-semibold">
<Trans i18nKey="teams:home.membersBillingButtonTitle" />
</span>
<p className="text-muted-foreground text-sm">
<Trans i18nKey="teams:home.membersBillingButtonDescription" />
</p>
</Card>
</div> </div>
</div> </div>
</div> </>
); );
} }

View File

@@ -1,8 +1,14 @@
import React from 'react'; import React from 'react';
import { Database } from '@/packages/supabase/src/database.types';
import Isikukood from 'isikukood';
import { Clock, TrendingUp, User } from 'lucide-react'; import { Clock, TrendingUp, User } from 'lucide-react';
import { bmiFromMetric } from '~/lib/utils'; import {
bmiFromMetric,
getBmiBackgroundColor,
getBmiStatus,
} from '~/lib/utils';
import { TeamAccountStatisticsProps } from '../../_components/team-account-statistics'; import { TeamAccountStatisticsProps } from '../../_components/team-account-statistics';
@@ -14,66 +20,89 @@ interface AccountHealthDetailsField {
color?: string; color?: string;
className?: string; className?: string;
}>; }>;
normStatus: NormStatus; iconBg: string;
}
export enum NormStatus {
CRITICAL = 'CRITICAL',
WARNING = 'WARNING',
NORMAL = 'NORMAL',
} }
export const getAccountHealthDetailsFields = ( export const getAccountHealthDetailsFields = (
memberParams: TeamAccountStatisticsProps['memberParams'], memberParams: TeamAccountStatisticsProps['memberParams'],
bmiThresholds: Omit<
Database['medreport']['Tables']['bmi_thresholds']['Row'],
'id'
>[],
members: Database['medreport']['Functions']['get_account_members']['Returns'],
): AccountHealthDetailsField[] => { ): AccountHealthDetailsField[] => {
const averageBMI = ( const avarageWeight =
memberParams.reduce((sum, { height, weight }) => { memberParams.reduce((sum, r) => sum + r.weight!, 0) / memberParams.length;
return bmiFromMetric(weight ?? 0, height ?? 0) + sum; const avarageHeight =
}, 0) / memberParams.length memberParams.reduce((sum, r) => sum + r.height!, 0) / memberParams.length;
).toFixed(0); const avarageAge =
members.reduce((sum, r) => {
const person = new Isikukood(r.personal_code);
return sum + person.getAge();
}, 0) / members.length;
const numberOfMaleMembers = members.filter((r) => {
const person = new Isikukood(r.personal_code);
return person.getGender() === 'male';
}).length;
const numberOfFemaleMembers = members.filter((r) => {
const person = new Isikukood(r.personal_code);
return person.getGender() === 'female';
}).length;
const averageBMI = bmiFromMetric(avarageWeight, avarageHeight);
const bmiStatus = getBmiStatus(bmiThresholds, {
age: avarageAge,
height: avarageHeight,
weight: avarageWeight,
});
const malePercentage = members.length
? (numberOfMaleMembers / members.length) * 100
: 0;
const femalePercentage = members.length
? (numberOfFemaleMembers / members.length) * 100
: 0;
return [ return [
{ {
title: 'teams:healthDetails.women', title: 'teams:healthDetails.women',
value: `50% (${memberParams.length})`, value: `${femalePercentage}% (${numberOfFemaleMembers})`,
Icon: User, Icon: User,
normStatus: NormStatus.NORMAL, iconBg: 'bg-success',
}, },
{ {
title: 'teams:healthDetails.men', title: 'teams:healthDetails.men',
value: `50% (${memberParams.length})`, value: `${malePercentage}% (${numberOfMaleMembers})`,
Icon: User, Icon: User,
normStatus: NormStatus.NORMAL, iconBg: 'bg-success',
}, },
{ {
title: 'teams:healthDetails.avgAge', title: 'teams:healthDetails.avgAge',
value: '56', value: avarageAge.toFixed(0),
Icon: Clock, Icon: Clock,
normStatus: NormStatus.NORMAL, iconBg: 'bg-success',
}, },
{ {
title: 'teams:healthDetails.bmi', title: 'teams:healthDetails.bmi',
value: averageBMI, value: averageBMI,
Icon: TrendingUp, Icon: TrendingUp,
normStatus: NormStatus.WARNING, iconBg: getBmiBackgroundColor(bmiStatus),
}, },
{ {
title: 'teams:healthDetails.cholesterol', title: 'teams:healthDetails.cholesterol',
value: '6.1', value: '-',
Icon: TrendingUp, Icon: TrendingUp,
normStatus: NormStatus.WARNING, iconBg: 'bg-warning',
}, },
{ {
title: 'teams:healthDetails.vitaminD', title: 'teams:healthDetails.vitaminD',
value: '76', value: '-',
Icon: TrendingUp, Icon: TrendingUp,
normStatus: NormStatus.NORMAL, iconBg: 'bg-warning',
}, },
{ {
title: 'teams:healthDetails.smokers', title: 'teams:healthDetails.smokers',
value: '22%', value: '-',
Icon: TrendingUp, Icon: TrendingUp,
normStatus: NormStatus.CRITICAL, iconBg: 'bg-warning',
}, },
]; ];
}; };

View File

@@ -8,16 +8,12 @@ import { z } from 'zod';
import { LineItemSchema } from '@kit/billing'; import { LineItemSchema } from '@kit/billing';
import { getBillingGatewayProvider } from '@kit/billing-gateway'; import { getBillingGatewayProvider } from '@kit/billing-gateway';
import { appConfig, billingConfig, pathsConfig } from '@kit/shared/config';
import { getLogger } from '@kit/shared/logger'; import { getLogger } from '@kit/shared/logger';
import { requireUser } from '@kit/supabase/require-user'; import { requireUser } from '@kit/supabase/require-user';
import { getSupabaseServerClient } from '@kit/supabase/server-client'; import { getSupabaseServerClient } from '@kit/supabase/server-client';
import { createTeamAccountsApi } from '@kit/team-accounts/api'; import { createTeamAccountsApi } from '@kit/team-accounts/api';
import appConfig from '@kit/shared/config/app.config';
import { billingConfig } from '@kit/shared/config';
import { pathsConfig } from '@kit/shared/config';
import { TeamCheckoutSchema } from '../schema/team-billing.schema'; import { TeamCheckoutSchema } from '../schema/team-billing.schema';
export function createTeamBillingService(client: SupabaseClient<Database>) { export function createTeamBillingService(client: SupabaseClient<Database>) {

View File

@@ -5,19 +5,21 @@ import { cookies } from 'next/headers';
import { z } from 'zod'; import { z } from 'zod';
import { AppLogo } from '@kit/shared/components/app-logo'; import { AppLogo } from '@kit/shared/components/app-logo';
import { getTeamAccountSidebarConfig } from '@kit/shared/config'; import { getTeamAccountSidebarConfig, pathsConfig } from '@kit/shared/config';
import { import {
CompanyGuard, CompanyGuard,
TeamAccountWorkspaceContextProvider, TeamAccountWorkspaceContextProvider,
} from '@kit/team-accounts/components'; } from '@kit/team-accounts/components';
import { Page, PageMobileNavigation, PageNavigation } from '@kit/ui/page'; import { Page, PageMobileNavigation, PageNavigation } from '@kit/ui/page';
import { SidebarProvider } from '@kit/ui/shadcn-sidebar'; import { SidebarProvider } from '@kit/ui/shadcn-sidebar';
import { withI18n } from '~/lib/i18n/with-i18n';
// local imports // local imports
import { TeamAccountLayoutMobileNavigation } from './_components/team-account-layout-mobile-navigation'; import { TeamAccountLayoutMobileNavigation } from './_components/team-account-layout-mobile-navigation';
import { TeamAccountLayoutSidebar } from './_components/team-account-layout-sidebar'; import { TeamAccountLayoutSidebar } from './_components/team-account-layout-sidebar';
import { TeamAccountNavigationMenu } from './_components/team-account-navigation-menu'; import { TeamAccountNavigationMenu } from './_components/team-account-navigation-menu';
import { loadTeamWorkspace } from './_lib/server/team-account-workspace.loader'; import { loadTeamWorkspace } from './_lib/server/team-account-workspace.loader';
import { withI18n } from '~/lib/i18n/with-i18n';
type TeamWorkspaceLayoutProps = React.PropsWithChildren<{ type TeamWorkspaceLayoutProps = React.PropsWithChildren<{
params: Promise<{ account: string }>; params: Promise<{ account: string }>;
@@ -66,7 +68,7 @@ function SidebarLayout({
</PageNavigation> </PageNavigation>
<PageMobileNavigation className={'flex items-center justify-between'}> <PageMobileNavigation className={'flex items-center justify-between'}>
<AppLogo /> <AppLogo href={pathsConfig.app.home} />
<div className={'flex space-x-4'}> <div className={'flex space-x-4'}>
<TeamAccountLayoutMobileNavigation <TeamAccountLayoutMobileNavigation
@@ -109,7 +111,7 @@ function HeaderLayout({
</PageNavigation> </PageNavigation>
<PageMobileNavigation className={'flex items-center justify-between'}> <PageMobileNavigation className={'flex items-center justify-between'}>
<AppLogo /> <AppLogo href={pathsConfig.app.home} />
<div className={'group-data-[mobile:hidden]'}> <div className={'group-data-[mobile:hidden]'}>
<TeamAccountLayoutMobileNavigation <TeamAccountLayoutMobileNavigation
@@ -134,7 +136,7 @@ function HeaderLayout({
<PageMobileNavigation <PageMobileNavigation
className={'flex items-center justify-between'} className={'flex items-center justify-between'}
> >
<AppLogo /> <AppLogo href={pathsConfig.app.home} />
<div className={'flex space-x-4'}> <div className={'flex space-x-4'}>
<TeamAccountLayoutMobileNavigation <TeamAccountLayoutMobileNavigation
userId={data.user.id} userId={data.user.id}

View File

@@ -2,18 +2,21 @@
import { use } from 'react'; import { use } from 'react';
import { createAccountsApi } from '@/packages/features/accounts/src/server/api';
import { CompanyGuard } from '@/packages/features/team-accounts/src/components'; import { CompanyGuard } from '@/packages/features/team-accounts/src/components';
import { createTeamAccountsApi } from '@/packages/features/team-accounts/src/server/api'; import { createTeamAccountsApi } from '@/packages/features/team-accounts/src/server/api';
import { getSupabaseServerClient } from '@/packages/supabase/src/clients/server-client'; import { getSupabaseServerClient } from '@/packages/supabase/src/clients/server-client';
import { PageBody } from '@kit/ui/page'; import { PageBody } from '@kit/ui/page';
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 {
PageViewAction,
createPageViewLog,
} from '~/lib/services/audit/pageView.service';
import { Dashboard } from './_components/dashboard'; import { Dashboard } from './_components/dashboard';
import { TeamAccountLayoutPageHeader } from './_components/team-account-layout-page-header';
import { withI18n } from '~/lib/i18n/with-i18n';
interface TeamAccountHomePageProps { interface TeamAccountHomePageProps {
params: Promise<{ account: string }>; params: Promise<{ account: string }>;
@@ -31,25 +34,32 @@ export const generateMetadata = async () => {
function TeamAccountHomePage({ params }: TeamAccountHomePageProps) { function TeamAccountHomePage({ params }: TeamAccountHomePageProps) {
const account = use(params).account; const account = use(params).account;
const client = getSupabaseServerClient(); const client = getSupabaseServerClient();
const api = createTeamAccountsApi(client); const teamAccountsApi = createTeamAccountsApi(client);
const teamAccount = use(api.getTeamAccount(account)); const accountsApi = createAccountsApi(client);
const { memberParams } = use(api.getMembers(account)); const teamAccount = use(teamAccountsApi.getTeamAccount(account));
const { memberParams, members } = use(teamAccountsApi.getMembers(account));
const bmiThresholds = use(accountsApi.fetchBmiThresholds());
const companyParams = use(
teamAccountsApi.getTeamAccountParams(teamAccount.id),
);
use(
createPageViewLog({
accountId: teamAccount.id,
action: PageViewAction.VIEW_TEAM_ACCOUNT_DASHBOARD,
}),
);
return ( return (
<> <PageBody>
<TeamAccountLayoutPageHeader <Dashboard
title={ teamAccount={teamAccount}
<Trans memberParams={memberParams}
i18nKey={'teams:home.headerTitle'} bmiThresholds={bmiThresholds}
values={{ companyName: account }} members={members}
/> companyParams={companyParams}
}
/> />
</PageBody>
<PageBody>
<Dashboard teamAccount={teamAccount} memberParams={memberParams} />
</PageBody>
</>
); );
} }

View File

@@ -1,6 +1,6 @@
import { MetadataRoute } from 'next'; import { MetadataRoute } from 'next';
import appConfig from '@kit/shared/config/app.config'; import { appConfig } from '@kit/shared/config';
export default function robots(): MetadataRoute.Robots { export default function robots(): MetadataRoute.Robots {
return { return {

View File

@@ -1,18 +1,17 @@
import Link from 'next/link'; import Link from 'next/link';
import SelectAnalysisPackages from '@kit/shared/components/select-analysis-packages';
import { CaretRightIcon } from '@radix-ui/react-icons'; import { CaretRightIcon } from '@radix-ui/react-icons';
import { Scale } from 'lucide-react'; import { Scale } from 'lucide-react';
import { MedReportLogo } from '@kit/shared/components/med-report-logo';
import SelectAnalysisPackages from '@kit/shared/components/select-analysis-packages';
import { pathsConfig } from '@kit/shared/config';
import { Button } from '@kit/ui/button'; import { Button } from '@kit/ui/button';
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 { MedReportLogo } from '../../components/med-report-logo';
import pathsConfig from '../../config/paths.config';
import ComparePackagesModal from '../home/(user)/_components/compare-packages-modal'; import ComparePackagesModal from '../home/(user)/_components/compare-packages-modal';
import { loadAnalysisPackages } from '../home/(user)/_lib/server/load-analysis-packages'; import { loadAnalysisPackages } from '../home/(user)/_lib/server/load-analysis-packages';

View File

@@ -1,8 +1,7 @@
import { getServerSideSitemap } from 'next-sitemap'; import { getServerSideSitemap } from 'next-sitemap';
import { createCmsClient } from '@kit/cms'; import { createCmsClient } from '@kit/cms';
import { appConfig } from '@kit/shared/config';
import appConfig from '@kit/shared/config/app.config';
/** /**
* @description The maximum age of the sitemap in seconds. * @description The maximum age of the sitemap in seconds.

View File

@@ -0,0 +1,46 @@
import { getSupabaseServerClient } from '@kit/supabase/server-client';
export enum DoctorPageViewAction {
VIEW_ANALYSIS_RESULTS = 'VIEW_ANALYSIS_RESULTS',
VIEW_DASHBOARD = 'VIEW_DASHBOARD',
VIEW_OPEN_JOBS = 'VIEW_OPEN_JOBS',
VIEW_OWN_JOBS = 'VIEW_OWN_JOBS',
VIEW_DONE_JOBS = 'VIEW_DONE_JOBS',
}
export const createDoctorPageViewLog = async ({
action,
recordKey,
dataOwnerUserId,
}: {
action: DoctorPageViewAction;
recordKey?: string;
dataOwnerUserId?: string;
}) => {
try {
const supabase = getSupabaseServerClient();
const {
data: { user },
error: userError,
} = await supabase.auth.getUser();
if (userError || !user) {
console.error('No authenticated user found; skipping audit insert');
return;
}
await supabase
.schema('audit')
.from('doctor_page_views')
.insert({
viewer_user_id: user.id,
data_owner_user_id: dataOwnerUserId,
viewed_record_key: recordKey,
action,
})
.throwOnError();
} catch (error) {
console.error('Failed to insert doctor page view log', error);
}
};

View File

@@ -1,9 +1,10 @@
import { getSupabaseServerClient } from '@kit/supabase/server-client'; import { getSupabaseServerClient } from '@kit/supabase/server-client';
export enum PAGE_VIEW_ACTION { export enum PageViewAction {
VIEW_ANALYSIS_RESULTS = 'VIEW_ANALYSIS_RESULTS', VIEW_ANALYSIS_RESULTS = 'VIEW_ANALYSIS_RESULTS',
REGISTRATION_SUCCESS = 'REGISTRATION_SUCCESS', REGISTRATION_SUCCESS = 'REGISTRATION_SUCCESS',
VIEW_ORDER_ANALYSIS = 'VIEW_ORDER_ANALYSIS', VIEW_ORDER_ANALYSIS = 'VIEW_ORDER_ANALYSIS',
VIEW_TEAM_ACCOUNT_DASHBOARD = 'VIEW_TEAM_ACCOUNT_DASHBOARD',
} }
export const createPageViewLog = async ({ export const createPageViewLog = async ({
@@ -11,7 +12,7 @@ export const createPageViewLog = async ({
action, action,
}: { }: {
accountId: string; accountId: string;
action: PAGE_VIEW_ACTION; action: PageViewAction;
}) => { }) => {
try { try {
const supabase = getSupabaseServerClient(); const supabase = getSupabaseServerClient();

View File

@@ -673,6 +673,7 @@ async function syncPrivateMessage({
unit: element.Mootyhik ?? null, unit: element.Mootyhik ?? null,
original_response_element: element, original_response_element: element,
analysis_name: element.UuringNimi || element.KNimetus, analysis_name: element.UuringNimi || element.KNimetus,
comment: element.UuringuKommentaar
})), })),
); );
} }

View File

@@ -71,15 +71,13 @@ export function getBmiStatus(
export function getBmiBackgroundColor(bmiStatus: BmiCategory | null): string { export function getBmiBackgroundColor(bmiStatus: BmiCategory | null): string {
switch (bmiStatus) { switch (bmiStatus) {
case BmiCategory.UNDER_WEIGHT: case BmiCategory.UNDER_WEIGHT:
case BmiCategory.OVER_WEIGHT:
return 'bg-warning'; return 'bg-warning';
case BmiCategory.NORMAL: case BmiCategory.NORMAL:
return 'bg-success'; return 'bg-success';
case BmiCategory.OVER_WEIGHT:
return 'bg-warning';
case BmiCategory.VERY_OVERWEIGHT: case BmiCategory.VERY_OVERWEIGHT:
return 'bg-destructive';
case BmiCategory.OBESE: case BmiCategory.OBESE:
return 'bg-error'; return 'bg-destructive';
default: default:
return 'bg-success'; return 'bg-success';
} }

View File

@@ -1,6 +1,6 @@
'use server'; 'use server';
import { revalidatePath, revalidateTag } from 'next/cache'; 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';
@@ -13,6 +13,7 @@ import {
doctorJobSelectSchema, doctorJobSelectSchema,
doctorJobUnselectSchema, doctorJobUnselectSchema,
} from '../schema/doctor-analysis.schema'; } from '../schema/doctor-analysis.schema';
import { ErrorReason } from '../schema/error.type';
import { import {
selectJob, selectJob,
submitFeedback, submitFeedback,
@@ -29,13 +30,28 @@ export const selectJobAction = doctorAction(
async ({ analysisOrderId, userId }: DoctorJobSelect) => { async ({ analysisOrderId, userId }: DoctorJobSelect) => {
const logger = await getLogger(); const logger = await getLogger();
logger.info({ analysisOrderId }, `Selecting new job`); try {
logger.info({ analysisOrderId }, `Selecting new job`);
await selectJob(analysisOrderId, userId); await selectJob(analysisOrderId, userId);
logger.info({ analysisOrderId }, `Successfully selected`);
logger.info({ analysisOrderId }, `Successfully selected`); revalidateDoctorAnalysis();
return { success: true };
return { success: true }; } catch (e) {
logger.error('Failed to select job', e);
if (e instanceof Error) {
revalidateDoctorAnalysis();
return {
success: false,
reason:
e['message'] === ErrorReason.JOB_ASSIGNED
? ErrorReason.JOB_ASSIGNED
: ErrorReason.UNKNOWN,
};
}
return { success: false, reason: ErrorReason.UNKNOWN };
}
}, },
{ {
schema: doctorJobSelectSchema, schema: doctorJobSelectSchema,
@@ -51,17 +67,21 @@ export const unselectJobAction = doctorAction(
enhanceAction( enhanceAction(
async ({ analysisOrderId }: DoctorJobUnselect) => { async ({ analysisOrderId }: DoctorJobUnselect) => {
const logger = await getLogger(); const logger = await getLogger();
try {
logger.info({ analysisOrderId }, `Removing doctor from job`);
logger.info({ analysisOrderId }, `Removing doctor from job`); await unselectJob(analysisOrderId);
await unselectJob(analysisOrderId); logger.info(
{ analysisOrderId },
logger.info( `Successfully removed current doctor from job`,
{ analysisOrderId }, );
`Successfully removed current doctor from job`, revalidateDoctorAnalysis();
); return { success: true };
} catch (e) {
return { success: true }; logger.error('Failed to unselect job', e);
return { success: false, reason: ErrorReason.UNKNOWN };
}
}, },
{ {
schema: doctorJobUnselectSchema, schema: doctorJobUnselectSchema,
@@ -88,23 +108,28 @@ export const giveFeedbackAction = doctorAction(
}) => { }) => {
const logger = await getLogger(); const logger = await getLogger();
logger.info( try {
{ analysisOrderId }, logger.info(
`Submitting feedback for analysis order...`, { analysisOrderId },
); `Submitting feedback for analysis order...`,
);
const result = await submitFeedback( await submitFeedback(analysisOrderId, userId, feedbackValue, status);
analysisOrderId, logger.info({ analysisOrderId }, `Successfully submitted feedback`);
userId,
feedbackValue,
status,
);
logger.info({ analysisOrderId }, `Successfully submitted feedback`);
return result; revalidateDoctorAnalysis();
return { success: true };
} catch (e) {
logger.error('Failed to give feedback', e);
return { success: false, reason: ErrorReason.UNKNOWN };
}
}, },
{ {
schema: doctorAnalysisFeedbackSchema, schema: doctorAnalysisFeedbackSchema,
}, },
), ),
); );
function revalidateDoctorAnalysis() {
revalidatePath('/doctor/analysis');
}

View File

@@ -66,6 +66,27 @@ export const AnalysisResponseSchema = z.object({
updated_at: z.string().nullable(), updated_at: z.string().nullable(),
analysis_name: z.string().nullable(), analysis_name: z.string().nullable(),
analysis_responses: AnalysisResponsesSchema, analysis_responses: AnalysisResponsesSchema,
comment: z.string().nullable(),
latestPreviousAnalysis: z
.object({
id: z.number(),
analysis_response_id: z.number(),
analysis_element_original_id: z.string(),
unit: z.string().nullable(),
response_value: z.number(),
response_time: z.string(),
norm_upper: z.number().nullable(),
norm_upper_included: z.boolean().nullable(),
norm_lower: z.number().nullable(),
norm_lower_included: z.boolean().nullable(),
norm_status: z.number().nullable(),
created_at: z.string(),
updated_at: z.string().nullable(),
analysis_name: z.string().nullable(),
comment: z.string().nullable(),
})
.optional()
.nullable(),
}); });
export type AnalysisResponse = z.infer<typeof AnalysisResponseSchema>; export type AnalysisResponse = z.infer<typeof AnalysisResponseSchema>;

View File

@@ -15,18 +15,19 @@ export type DoctorJobUnselect = z.infer<typeof doctorJobUnselectSchema>;
export const FeedbackStatus = z.enum(['STARTED', 'DRAFT', 'COMPLETED']); export const FeedbackStatus = z.enum(['STARTED', 'DRAFT', 'COMPLETED']);
export const doctorAnalysisFeedbackFormSchema = z.object({ export const doctorAnalysisFeedbackFormSchema = z.object({
feedbackValue: z.string().min(15), feedbackValue: z.string().min(10, { message: 'doctor:feedbackLengthError' }),
userId: z.string().uuid(), userId: z.string().uuid(),
}); });
export type DoctorAnalysisFeedbackForm = z.infer< export type DoctorAnalysisFeedbackForm = z.infer<
typeof doctorAnalysisFeedbackFormSchema typeof doctorAnalysisFeedbackFormSchema
>; >;
export const doctorAnalysisFeedbackSchema = z.object({ export const doctorAnalysisFeedbackSchema = z.object({
feedbackValue: z.string().min(15), feedbackValue: z.string(),
userId: z.string().uuid(), userId: z.string().uuid(),
analysisOrderId: z.number(), analysisOrderId: z.number(),
status: FeedbackStatus, status: FeedbackStatus,
}); });
export type DoctorAnalysisFeedback = z.infer< export type DoctorAnalysisFeedback = z.infer<
typeof doctorAnalysisFeedbackSchema typeof doctorAnalysisFeedbackSchema
>; >;

View File

@@ -0,0 +1,4 @@
export enum ErrorReason {
JOB_ASSIGNED = 'JOB_ASSIGNED',
UNKNOWN = 'UNKNOWN',
}

View File

@@ -1,5 +1,7 @@
import 'server-only'; import 'server-only';
import { isBefore } from 'date-fns';
import { getSupabaseServerClient } from '@kit/supabase/server-client'; import { getSupabaseServerClient } from '@kit/supabase/server-client';
import { AnalysisResultDetails } from '../schema/doctor-analysis-detail-view.schema'; import { AnalysisResultDetails } from '../schema/doctor-analysis-detail-view.schema';
@@ -10,6 +12,7 @@ import {
PaginationParams, PaginationParams,
ResponseTable, ResponseTable,
} from '../schema/doctor-analysis.schema'; } from '../schema/doctor-analysis.schema';
import { ErrorReason } from '../schema/error.type';
async function enrichAnalysisData(analysisResponses?: AnalysisResponseBase[]) { async function enrichAnalysisData(analysisResponses?: AnalysisResponseBase[]) {
const supabase = getSupabaseServerClient(); const supabase = getSupabaseServerClient();
@@ -360,7 +363,7 @@ export async function getAnalysisResultsForDoctor(
): Promise<AnalysisResultDetails> { ): Promise<AnalysisResultDetails> {
const supabase = getSupabaseServerClient(); const supabase = getSupabaseServerClient();
const { data: analysisResponse, error } = await supabase const { data: analysisResponseElements, error } = await supabase
.schema('medreport') .schema('medreport')
.from(`analysis_response_elements`) .from(`analysis_response_elements`)
.select( .select(
@@ -373,20 +376,26 @@ export async function getAnalysisResultsForDoctor(
throw new Error('Something went wrong.'); throw new Error('Something went wrong.');
} }
const firstAnalysisResponse = analysisResponse?.[0]; const firstAnalysisResponse = analysisResponseElements?.[0];
const userId = firstAnalysisResponse?.analysis_responses.user_id; const userId = firstAnalysisResponse?.analysis_responses.user_id;
const medusaOrderId = const medusaOrderId =
firstAnalysisResponse?.analysis_responses?.analysis_order_id firstAnalysisResponse?.analysis_responses?.analysis_order_id
?.medusa_order_id; ?.medusa_order_id;
if (!analysisResponse?.length || !userId || !medusaOrderId) { if (!analysisResponseElements?.length || !userId || !medusaOrderId) {
throw new Error('Failed to retrieve full analysis data.'); throw new Error('Failed to retrieve full analysis data.');
} }
const responseElementAnalysisElementOriginalIds =
analysisResponseElements.map(
({ analysis_element_original_id }) => analysis_element_original_id,
);
const [ const [
{ data: medusaOrderItems, error: medusaOrderError }, { data: medusaOrderItems, error: medusaOrderError },
{ data: accountWithParams, error: accountError }, { data: accountWithParams, error: accountError },
{ data: doctorFeedback, error: feedbackError }, { data: doctorFeedback, error: feedbackError },
{ data: previousAnalyses, error: previousAnalysesError },
] = await Promise.all([ ] = await Promise.all([
supabase supabase
.schema('public') .schema('public')
@@ -403,7 +412,7 @@ export async function getAnalysisResultsForDoctor(
.eq('is_personal_account', true) .eq('is_personal_account', true)
.eq('primary_owner_user_id', userId) .eq('primary_owner_user_id', userId)
.limit(1), .limit(1),
await supabase supabase
.schema('medreport') .schema('medreport')
.from('doctor_analysis_feedback') .from('doctor_analysis_feedback')
.select(`*`) .select(`*`)
@@ -412,9 +421,39 @@ export async function getAnalysisResultsForDoctor(
firstAnalysisResponse.analysis_responses.analysis_order_id.id, firstAnalysisResponse.analysis_responses.analysis_order_id.id,
) )
.limit(1), .limit(1),
supabase
.schema('medreport')
.from('analysis_response_elements')
.select(
`
*,
analysis_responses!inner(
user_id
)
`,
)
.in(
'analysis_element_original_id',
responseElementAnalysisElementOriginalIds,
)
.not(
'analysis_response_id',
'eq',
firstAnalysisResponse.analysis_response_id,
)
.eq(
'analysis_responses.user_id',
firstAnalysisResponse.analysis_responses.user_id,
)
.order('response_time'),
]); ]);
if (medusaOrderError || accountError || feedbackError) { if (
medusaOrderError ||
accountError ||
feedbackError ||
previousAnalysesError
) {
throw new Error('Something went wrong.'); throw new Error('Something went wrong.');
} }
@@ -433,8 +472,25 @@ export async function getAnalysisResultsForDoctor(
account_params, account_params,
} = accountWithParams[0]; } = accountWithParams[0];
const analysisResponseElementsWithPreviousData = [];
for (const analysisResponseElement of analysisResponseElements) {
const latestPreviousAnalysis = previousAnalyses.find(
({ analysis_element_original_id, response_time }) =>
analysis_element_original_id ===
analysisResponseElement.analysis_element_original_id &&
isBefore(
new Date(response_time),
new Date(analysisResponseElement.response_time),
),
);
analysisResponseElementsWithPreviousData.push({
...analysisResponseElement,
latestPreviousAnalysis,
});
}
return { return {
analysisResponse, analysisResponse: analysisResponseElementsWithPreviousData,
order: { order: {
title: medusaOrderItems?.[0]?.item_id.product_title ?? '-', title: medusaOrderItems?.[0]?.item_id.product_title ?? '-',
isPackage: isPackage:
@@ -479,7 +535,7 @@ export async function selectJob(analysisOrderId: number, userId: string) {
const jobAssignedToUserId = existingFeedback?.[0]?.doctor_user_id; const jobAssignedToUserId = existingFeedback?.[0]?.doctor_user_id;
if (!!jobAssignedToUserId && jobAssignedToUserId !== user.id) { if (!!jobAssignedToUserId && jobAssignedToUserId !== user.id) {
throw new Error('Job already assigned to another doctor.'); throw new Error(ErrorReason.JOB_ASSIGNED);
} }
const { data, error } = await supabase const { data, error } = await supabase

View File

@@ -291,6 +291,7 @@ export class TeamAccountsApi {
.single(); .single();
if (error) { if (error) {
console.warn('Error fetching company params', error);
throw error; throw error;
} }

View File

@@ -18,7 +18,6 @@ import { VersionUpdater } from '@kit/ui/version-updater';
import { i18nResolver } from '../../../../lib/i18n/i18n.resolver'; import { i18nResolver } from '../../../../lib/i18n/i18n.resolver';
import { getI18nSettings } from '../../../../lib/i18n/i18n.settings'; import { getI18nSettings } from '../../../../lib/i18n/i18n.settings';
import { ReactQueryProvider } from './react-query-provider'; import { ReactQueryProvider } from './react-query-provider';
const captchaSiteKey = authConfig.captchaTokenSiteKey; const captchaSiteKey = authConfig.captchaTokenSiteKey;

View File

@@ -13,7 +13,7 @@ export function InfoTooltip({
content, content,
icon, icon,
}: { }: {
content?: string; content?: string | null;
icon?: JSX.Element; icon?: JSX.Element;
}) { }) {
if (!content) return null; if (!content) return null;

View File

@@ -42,6 +42,33 @@ export type Database = {
} }
Relationships: [] Relationships: []
} }
doctor_page_views: {
Row: {
action: Database["audit"]["Enums"]["doctor_page_view_action"]
created_at: string
data_owner_user_id: string | null
id: number
viewed_record_key: string | null
viewer_user_id: string
}
Insert: {
action: Database["audit"]["Enums"]["doctor_page_view_action"]
created_at?: string
data_owner_user_id?: string | null
id?: number
viewed_record_key?: string | null
viewer_user_id: string
}
Update: {
action?: Database["audit"]["Enums"]["doctor_page_view_action"]
created_at?: string
data_owner_user_id?: string | null
id?: number
viewed_record_key?: string | null
viewer_user_id?: string
}
Relationships: []
}
log_entries: { log_entries: {
Row: { Row: {
changed_at: string changed_at: string
@@ -204,6 +231,12 @@ export type Database = {
[_ in never]: never [_ in never]: never
} }
Enums: { Enums: {
doctor_page_view_action:
| "VIEW_ANALYSIS_RESULTS"
| "VIEW_DASHBOARD"
| "VIEW_OPEN_JOBS"
| "VIEW_OWN_JOBS"
| "VIEW_DONE_JOBS"
request_status: "SUCCESS" | "FAIL" request_status: "SUCCESS" | "FAIL"
sync_status: "SUCCESS" | "FAIL" sync_status: "SUCCESS" | "FAIL"
} }
@@ -574,6 +607,7 @@ export type Database = {
analysis_element_original_id: string analysis_element_original_id: string
analysis_name: string | null analysis_name: string | null
analysis_response_id: number analysis_response_id: number
comment: string | null
created_at: string created_at: string
id: number id: number
norm_lower: number | null norm_lower: number | null
@@ -591,6 +625,7 @@ export type Database = {
analysis_element_original_id: string analysis_element_original_id: string
analysis_name?: string | null analysis_name?: string | null
analysis_response_id: number analysis_response_id: number
comment?: string | null
created_at?: string created_at?: string
id?: number id?: number
norm_lower?: number | null norm_lower?: number | null
@@ -608,6 +643,7 @@ export type Database = {
analysis_element_original_id?: string analysis_element_original_id?: string
analysis_name?: string | null analysis_name?: string | null
analysis_response_id?: number analysis_response_id?: number
comment?: string | null
created_at?: string created_at?: string
id?: number id?: number
norm_lower?: number | null norm_lower?: number | null
@@ -1748,7 +1784,9 @@ export type Database = {
Returns: Json Returns: Json
} }
create_team_account: { create_team_account: {
Args: { account_name: string; new_personal_code: string } Args:
| { 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
@@ -1935,6 +1973,22 @@ export type Database = {
} }
Returns: undefined Returns: undefined
} }
update_analysis_order_status: {
Args: {
order_id: number
medusa_order_id_param: string
status_param: Database["medreport"]["Enums"]["analysis_order_status"]
}
Returns: {
analysis_element_ids: number[] | null
analysis_ids: number[] | null
created_at: string
id: number
medusa_order_id: string
status: Database["medreport"]["Enums"]["analysis_order_status"]
user_id: string
}
}
upsert_order: { upsert_order: {
Args: { Args: {
target_account_id: string target_account_id: string
@@ -7905,6 +7959,13 @@ export type CompositeTypes<
export const Constants = { export const Constants = {
audit: { audit: {
Enums: { Enums: {
doctor_page_view_action: [
"VIEW_ANALYSIS_RESULTS",
"VIEW_DASHBOARD",
"VIEW_OPEN_JOBS",
"VIEW_OWN_JOBS",
"VIEW_DONE_JOBS",
],
request_status: ["SUCCESS", "FAIL"], request_status: ["SUCCESS", "FAIL"],
sync_status: ["SUCCESS", "FAIL"], sync_status: ["SUCCESS", "FAIL"],
}, },

View File

@@ -21,6 +21,7 @@ export function useSignUpWithEmailAndPassword() {
const response = await client.auth.signUp({ const response = await client.auth.signUp({
...credentials, ...credentials,
options: { options: {
emailRedirectTo,
captchaToken, captchaToken,
}, },
}); });

130
pnpm-lock.yaml generated
View File

@@ -82,13 +82,13 @@ importers:
version: 1.1.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0) version: 1.1.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@medusajs/icons': '@medusajs/icons':
specifier: ^2.8.6 specifier: ^2.8.6
version: 2.8.7(react@19.1.0) version: 2.8.6(react@19.1.0)
'@medusajs/js-sdk': '@medusajs/js-sdk':
specifier: latest specifier: latest
version: 2.9.0(awilix@8.0.1) version: 2.8.7(awilix@8.0.1)
'@medusajs/ui': '@medusajs/ui':
specifier: latest specifier: latest
version: 4.0.19(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.8.3) version: 4.0.17(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.8.3)
'@nosecone/next': '@nosecone/next':
specifier: 1.0.0-beta.7 specifier: 1.0.0-beta.7
version: 1.0.0-beta.7(next@15.3.2(@babel/core@7.27.4)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)) version: 1.0.0-beta.7(next@15.3.2(@babel/core@7.27.4)(@opentelemetry/api@1.9.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))
@@ -182,10 +182,10 @@ importers:
version: link:tooling/typescript version: link:tooling/typescript
'@medusajs/types': '@medusajs/types':
specifier: latest specifier: latest
version: 2.9.0(awilix@8.0.1) version: 2.8.7(awilix@8.0.1)
'@medusajs/ui-preset': '@medusajs/ui-preset':
specifier: latest specifier: latest
version: 2.9.0(tailwindcss@4.1.7) version: 2.8.7(tailwindcss@4.1.7)
'@next/bundle-analyzer': '@next/bundle-analyzer':
specifier: 15.3.2 specifier: 15.3.2
version: 15.3.2 version: 15.3.2
@@ -251,7 +251,7 @@ importers:
version: link:../../tooling/typescript version: link:../../tooling/typescript
'@types/node': '@types/node':
specifier: ^22.15.18 specifier: ^22.15.18
version: 22.15.32 version: 22.15.30
packages/billing/core: packages/billing/core:
devDependencies: devDependencies:
@@ -275,7 +275,7 @@ importers:
devDependencies: devDependencies:
'@hookform/resolvers': '@hookform/resolvers':
specifier: ^5.0.1 specifier: ^5.0.1
version: 5.1.1(react-hook-form@7.58.0(react@19.1.0)) version: 5.0.1(react-hook-form@7.58.0(react@19.1.0))
'@kit/billing': '@kit/billing':
specifier: workspace:* specifier: workspace:*
version: link:../core version: link:../core
@@ -469,7 +469,7 @@ importers:
version: link:../wordpress version: link:../wordpress
'@types/node': '@types/node':
specifier: ^22.15.18 specifier: ^22.15.18
version: 22.15.32 version: 22.15.30
packages/cms/keystatic: packages/cms/keystatic:
dependencies: dependencies:
@@ -500,7 +500,7 @@ importers:
version: link:../../ui version: link:../../ui
'@types/node': '@types/node':
specifier: ^22.15.18 specifier: ^22.15.18
version: 22.15.32 version: 22.15.30
'@types/react': '@types/react':
specifier: 19.1.4 specifier: 19.1.4
version: 19.1.4 version: 19.1.4
@@ -539,7 +539,7 @@ importers:
version: link:../../ui version: link:../../ui
'@types/node': '@types/node':
specifier: ^22.15.18 specifier: ^22.15.18
version: 22.15.32 version: 22.15.30
'@types/react': '@types/react':
specifier: 19.1.4 specifier: 19.1.4
version: 19.1.4 version: 19.1.4
@@ -610,7 +610,7 @@ importers:
devDependencies: devDependencies:
'@hookform/resolvers': '@hookform/resolvers':
specifier: ^5.0.1 specifier: ^5.0.1
version: 5.1.1(react-hook-form@7.58.0(react@19.1.0)) version: 5.0.1(react-hook-form@7.58.0(react@19.1.0))
'@kit/billing-gateway': '@kit/billing-gateway':
specifier: workspace:* specifier: workspace:*
version: link:../../billing/gateway version: link:../../billing/gateway
@@ -685,7 +685,7 @@ importers:
devDependencies: devDependencies:
'@hookform/resolvers': '@hookform/resolvers':
specifier: ^5.0.1 specifier: ^5.0.1
version: 5.1.1(react-hook-form@7.58.0(react@19.1.0)) version: 5.0.1(react-hook-form@7.58.0(react@19.1.0))
'@kit/eslint-config': '@kit/eslint-config':
specifier: workspace:* specifier: workspace:*
version: link:../../../tooling/eslint version: link:../../../tooling/eslint
@@ -742,7 +742,7 @@ importers:
devDependencies: devDependencies:
'@hookform/resolvers': '@hookform/resolvers':
specifier: ^5.0.1 specifier: ^5.0.1
version: 5.1.1(react-hook-form@7.58.0(react@19.1.0)) version: 5.0.1(react-hook-form@7.58.0(react@19.1.0))
'@kit/eslint-config': '@kit/eslint-config':
specifier: workspace:* specifier: workspace:*
version: link:../../../tooling/eslint version: link:../../../tooling/eslint
@@ -850,10 +850,10 @@ importers:
version: 2.2.4(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) version: 2.2.4(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)
'@medusajs/js-sdk': '@medusajs/js-sdk':
specifier: latest specifier: latest
version: 2.9.0(awilix@8.0.1) version: 2.8.7(awilix@8.0.1)
'@medusajs/ui': '@medusajs/ui':
specifier: latest specifier: latest
version: 4.0.19(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)(typescript@5.8.3) version: 4.0.17(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)(typescript@5.8.3)
'@radix-ui/react-accordion': '@radix-ui/react-accordion':
specifier: ^1.2.1 specifier: ^1.2.1
version: 1.2.10(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) version: 1.2.10(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)
@@ -899,10 +899,10 @@ importers:
version: 7.27.4 version: 7.27.4
'@medusajs/types': '@medusajs/types':
specifier: latest specifier: latest
version: 2.9.0(awilix@8.0.1) version: 2.8.7(awilix@8.0.1)
'@medusajs/ui-preset': '@medusajs/ui-preset':
specifier: latest specifier: latest
version: 2.9.0(tailwindcss@3.4.17(ts-node@10.9.2(@types/node@17.0.21)(typescript@5.8.3))) version: 2.8.7(tailwindcss@3.4.17(ts-node@10.9.2(@types/node@17.0.21)(typescript@5.8.3)))
'@types/lodash': '@types/lodash':
specifier: ^4.14.195 specifier: ^4.14.195
version: 4.17.17 version: 4.17.17
@@ -993,7 +993,7 @@ importers:
devDependencies: devDependencies:
'@hookform/resolvers': '@hookform/resolvers':
specifier: ^5.0.1 specifier: ^5.0.1
version: 5.1.1(react-hook-form@7.58.0(react@19.1.0)) version: 5.0.1(react-hook-form@7.58.0(react@19.1.0))
'@kit/accounts': '@kit/accounts':
specifier: workspace:* specifier: workspace:*
version: link:../accounts version: link:../accounts
@@ -1135,7 +1135,7 @@ importers:
version: link:../../../tooling/typescript version: link:../../../tooling/typescript
'@types/node': '@types/node':
specifier: ^22.15.18 specifier: ^22.15.18
version: 22.15.32 version: 22.15.30
packages/mailers/nodemailer: packages/mailers/nodemailer:
dependencies: dependencies:
@@ -1175,7 +1175,7 @@ importers:
version: link:../../../tooling/typescript version: link:../../../tooling/typescript
'@types/node': '@types/node':
specifier: ^22.15.18 specifier: ^22.15.18
version: 22.15.32 version: 22.15.30
packages/mailers/shared: packages/mailers/shared:
devDependencies: devDependencies:
@@ -1324,7 +1324,7 @@ importers:
devDependencies: devDependencies:
'@hookform/resolvers': '@hookform/resolvers':
specifier: ^5.0.1 specifier: ^5.0.1
version: 5.1.1(react-hook-form@7.58.0(react@19.1.0)) version: 5.0.1(react-hook-form@7.58.0(react@19.1.0))
'@kit/email-templates': '@kit/email-templates':
specifier: workspace:* specifier: workspace:*
version: link:../email-templates version: link:../email-templates
@@ -1429,7 +1429,7 @@ importers:
dependencies: dependencies:
'@hookform/resolvers': '@hookform/resolvers':
specifier: ^5.0.1 specifier: ^5.0.1
version: 5.1.1(react-hook-form@7.58.0(react@19.1.0)) version: 5.0.1(react-hook-form@7.58.0(react@19.1.0))
'@radix-ui/react-accordion': '@radix-ui/react-accordion':
specifier: 1.2.10 specifier: 1.2.10
version: 1.2.10(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) version: 1.2.10(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
@@ -1476,7 +1476,7 @@ importers:
specifier: ^1.1.6 specifier: ^1.1.6
version: 1.1.7(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) version: 1.1.7(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@radix-ui/react-slot': '@radix-ui/react-slot':
specifier: ^1.2.3 specifier: ^1.2.2
version: 1.2.3(@types/react@19.1.4)(react@19.1.0) version: 1.2.3(@types/react@19.1.4)(react@19.1.0)
'@radix-ui/react-switch': '@radix-ui/react-switch':
specifier: ^1.2.4 specifier: ^1.2.4
@@ -1510,7 +1510,7 @@ importers:
version: 2.15.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0) version: 2.15.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
tailwind-merge: tailwind-merge:
specifier: ^3.3.0 specifier: ^3.3.0
version: 3.3.1 version: 3.3.0
devDependencies: devDependencies:
'@kit/eslint-config': '@kit/eslint-config':
specifier: workspace:* specifier: workspace:*
@@ -2037,6 +2037,11 @@ packages:
react: ^18 || ^19 || ^19.0.0-rc react: ^18 || ^19 || ^19.0.0-rc
react-dom: ^18 || ^19 || ^19.0.0-rc react-dom: ^18 || ^19 || ^19.0.0-rc
'@hookform/resolvers@5.0.1':
resolution: {integrity: sha512-u/+Jp83luQNx9AdyW2fIPGY6Y7NG68eN2ZW8FOJYL+M0i4s49+refdJdOp/A9n9HFQtQs3HIDHQvX3ZET2o7YA==}
peerDependencies:
react-hook-form: ^7.55.0
'@hookform/resolvers@5.1.1': '@hookform/resolvers@5.1.1':
resolution: {integrity: sha512-J/NVING3LMAEvexJkyTLjruSm7aOFx7QX21pzkiJfMoNG0wl5aFEjLTl7ay7IQb9EWY6AkrBy7tHL2Alijpdcg==} resolution: {integrity: sha512-J/NVING3LMAEvexJkyTLjruSm7aOFx7QX21pzkiJfMoNG0wl5aFEjLTl7ay7IQb9EWY6AkrBy7tHL2Alijpdcg==}
peerDependencies: peerDependencies:
@@ -2383,22 +2388,22 @@ packages:
react: ^17.0.2 || ^18.0.0 || ^19.0 react: ^17.0.2 || ^18.0.0 || ^19.0
react-dom: ^17.0.2 || ^18.0.0 || ^19.0 react-dom: ^17.0.2 || ^18.0.0 || ^19.0
'@medusajs/icons@2.8.6':
resolution: {integrity: sha512-k3X1nA1L0eoR30tfAzCxTtpaE1h28K2qmuNyangOoBJObHkaD+gNIi3AG+2iLlmIrByzfCgzP0JvhzrtFFha4Q==}
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc
'@medusajs/icons@2.8.7': '@medusajs/icons@2.8.7':
resolution: {integrity: sha512-zGkAokqWBNJ1PcTktCPSMT5spIIjv8Pba88BXvfcbblG5cUbMSvvJ2v/BRODMFejQ9NqlboIeP0fo/9RzLpPHg==} resolution: {integrity: sha512-zGkAokqWBNJ1PcTktCPSMT5spIIjv8Pba88BXvfcbblG5cUbMSvvJ2v/BRODMFejQ9NqlboIeP0fo/9RzLpPHg==}
peerDependencies: peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc
'@medusajs/icons@2.9.0': '@medusajs/js-sdk@2.8.7':
resolution: {integrity: sha512-qzFyX8f87WjLBFr23aly5F8hmN/camZ2oVcqmP1XBK4HqOWWxrPPjABePomQixwm7XGkfQHZf+B2rnyIyjwfKA==} resolution: {integrity: sha512-ZGYMQOM7GHuKtxOvJ+wgKyC/fzLlyMu5nij4hIWIf2osZy7d6dpvEglcV6w9B0UgSEADJh1SZ7a22HOJdjjJ9A==}
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc
'@medusajs/js-sdk@2.9.0':
resolution: {integrity: sha512-5L5dN235k/EyNelAYrUZevjxULfhyswAtRH5oy6PETbb4ExBdFi//vFXSnOuWjj2YnZtrTwymJkeZSOMYuMxSg==}
engines: {node: '>=20'} engines: {node: '>=20'}
'@medusajs/types@2.9.0': '@medusajs/types@2.8.7':
resolution: {integrity: sha512-7YGHq7OuvmHfKCz9sXus7aMHIwZXt1B3QYOoH+7gziEA0JXB0WNhx2kbWt4qSjQxsrFtCkdQgqBiWHkgvkq2iA==} resolution: {integrity: sha512-8m/H9KkDUQz4YD+XkD/C63RfE/2elcdWf5G/KOK2QViTK0Jsd/Iw8Yy+T60pm0Lq/QQ925AfGH/Ji8UYNXjT8g==}
engines: {node: '>=20'} engines: {node: '>=20'}
peerDependencies: peerDependencies:
awilix: ^8.0.1 awilix: ^8.0.1
@@ -2410,13 +2415,13 @@ packages:
vite: vite:
optional: true optional: true
'@medusajs/ui-preset@2.9.0': '@medusajs/ui-preset@2.8.7':
resolution: {integrity: sha512-ykoM2UY2wKGZbVhiAW0bOvizqa5wxYk0C7aCyUu0DpoFS8KKSas8V4IfIBiav++0LUJjKZunJWzDu1qMyqQWTw==} resolution: {integrity: sha512-ro8BrYlqHh7iZvYKrxmJtLweJYYet+wYQQv0R3pyfxkkP0aQ09KDPo8yTwls11iuMC4cQHljekdaOyXtSR6ZiQ==}
peerDependencies: peerDependencies:
tailwindcss: '>=3.0.0' tailwindcss: '>=3.0.0'
'@medusajs/ui@4.0.19': '@medusajs/ui@4.0.17':
resolution: {integrity: sha512-iDy41IXHpYOLaM8aizZmuQjiQuFf0sKYK1CVwx1nsPvzXuuyJWGiTnoMiAhZ3NWgnf3dNDHFgnHlsU1k4zV2pQ==} resolution: {integrity: sha512-N5KtZXvns13jDiCE3ZgZLINQnlECYLf4Q4GFdbRhCjAFKFBRGyyeNKX+Zo2wBUZA2Oi4kockdxFfsZfBHh/ZhA==}
peerDependencies: peerDependencies:
react: ^18.0.0 || ^19.0.0 || ^19.0.0-rc react: ^18.0.0 || ^19.0.0 || ^19.0.0-rc
react-dom: ^18.0.0 || ^19.0.0 || ^19.0.0-rc react-dom: ^18.0.0 || ^19.0.0 || ^19.0.0-rc
@@ -5735,6 +5740,9 @@ packages:
'@types/node@17.0.21': '@types/node@17.0.21':
resolution: {integrity: sha512-DBZCJbhII3r90XbQxI8Y9IjjiiOGlZ0Hr32omXIZvwwZ7p4DMMXGrKXVyPfuoBOri9XNtL0UK69jYIBIsRX3QQ==} resolution: {integrity: sha512-DBZCJbhII3r90XbQxI8Y9IjjiiOGlZ0Hr32omXIZvwwZ7p4DMMXGrKXVyPfuoBOri9XNtL0UK69jYIBIsRX3QQ==}
'@types/node@22.15.30':
resolution: {integrity: sha512-6Q7lr06bEHdlfplU6YRbgG1SFBdlsfNC4/lX+SkhiTs0cpJkOElmWls8PxDFv4yY/xKb8Y6SO0OmSX4wgqTZbA==}
'@types/node@22.15.32': '@types/node@22.15.32':
resolution: {integrity: sha512-3jigKqgSjsH6gYZv2nEsqdXfZqIFGAV36XYYjf9KGZ3PSG+IhLecqPnI310RvjutyMwifE2hhhNEklOUrvx/wA==} resolution: {integrity: sha512-3jigKqgSjsH6gYZv2nEsqdXfZqIFGAV36XYYjf9KGZ3PSG+IhLecqPnI310RvjutyMwifE2hhhNEklOUrvx/wA==}
@@ -9627,6 +9635,9 @@ packages:
tailwind-merge@2.6.0: tailwind-merge@2.6.0:
resolution: {integrity: sha512-P+Vu1qXfzediirmHOC3xKGAYeZtPcV9g76X+xg2FD4tYgR71ewMA35Y3sCz3zhiN/dwefRpJX0yBcgwi1fXNQA==} resolution: {integrity: sha512-P+Vu1qXfzediirmHOC3xKGAYeZtPcV9g76X+xg2FD4tYgR71ewMA35Y3sCz3zhiN/dwefRpJX0yBcgwi1fXNQA==}
tailwind-merge@3.3.0:
resolution: {integrity: sha512-fyW/pEfcQSiigd5SNn0nApUOxx0zB/dm6UDU/rEwc2c3sX2smWUNbapHv+QRqLGVp9GWX3THIa7MUGPo+YkDzQ==}
tailwind-merge@3.3.1: tailwind-merge@3.3.1:
resolution: {integrity: sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g==} resolution: {integrity: sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g==}
@@ -10691,6 +10702,11 @@ snapshots:
react-dom: 19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106) react-dom: 19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106)
use-sync-external-store: 1.5.0(react@19.0.0-rc-66855b96-20241106) use-sync-external-store: 1.5.0(react@19.0.0-rc-66855b96-20241106)
'@hookform/resolvers@5.0.1(react-hook-form@7.58.0(react@19.1.0))':
dependencies:
'@standard-schema/utils': 0.3.0
react-hook-form: 7.58.0(react@19.1.0)
'@hookform/resolvers@5.1.1(react-hook-form@7.58.0(react@19.1.0))': '@hookform/resolvers@5.1.1(react-hook-form@7.58.0(react@19.1.0))':
dependencies: dependencies:
'@standard-schema/utils': 0.3.0 '@standard-schema/utils': 0.3.0
@@ -11255,21 +11271,21 @@ snapshots:
react: 19.1.0 react: 19.1.0
react-dom: 19.1.0(react@19.1.0) react-dom: 19.1.0(react@19.1.0)
'@medusajs/icons@2.8.6(react@19.1.0)':
dependencies:
react: 19.1.0
'@medusajs/icons@2.8.7(react@19.0.0-rc-66855b96-20241106)':
dependencies:
react: 19.0.0-rc-66855b96-20241106
'@medusajs/icons@2.8.7(react@19.1.0)': '@medusajs/icons@2.8.7(react@19.1.0)':
dependencies: dependencies:
react: 19.1.0 react: 19.1.0
'@medusajs/icons@2.9.0(react@19.0.0-rc-66855b96-20241106)': '@medusajs/js-sdk@2.8.7(awilix@8.0.1)':
dependencies: dependencies:
react: 19.0.0-rc-66855b96-20241106 '@medusajs/types': 2.8.7(awilix@8.0.1)
'@medusajs/icons@2.9.0(react@19.1.0)':
dependencies:
react: 19.1.0
'@medusajs/js-sdk@2.9.0(awilix@8.0.1)':
dependencies:
'@medusajs/types': 2.9.0(awilix@8.0.1)
fetch-event-stream: 0.1.5 fetch-event-stream: 0.1.5
qs: 6.14.0 qs: 6.14.0
transitivePeerDependencies: transitivePeerDependencies:
@@ -11277,26 +11293,26 @@ snapshots:
- ioredis - ioredis
- vite - vite
'@medusajs/types@2.9.0(awilix@8.0.1)': '@medusajs/types@2.8.7(awilix@8.0.1)':
dependencies: dependencies:
awilix: 8.0.1 awilix: 8.0.1
bignumber.js: 9.3.0 bignumber.js: 9.3.0
'@medusajs/ui-preset@2.9.0(tailwindcss@3.4.17(ts-node@10.9.2(@types/node@17.0.21)(typescript@5.8.3)))': '@medusajs/ui-preset@2.8.7(tailwindcss@3.4.17(ts-node@10.9.2(@types/node@17.0.21)(typescript@5.8.3)))':
dependencies: dependencies:
'@tailwindcss/forms': 0.5.10(tailwindcss@3.4.17(ts-node@10.9.2(@types/node@17.0.21)(typescript@5.8.3))) '@tailwindcss/forms': 0.5.10(tailwindcss@3.4.17(ts-node@10.9.2(@types/node@17.0.21)(typescript@5.8.3)))
tailwindcss: 3.4.17(ts-node@10.9.2(@types/node@17.0.21)(typescript@5.8.3)) tailwindcss: 3.4.17(ts-node@10.9.2(@types/node@17.0.21)(typescript@5.8.3))
tailwindcss-animate: 1.0.7(tailwindcss@3.4.17(ts-node@10.9.2(@types/node@17.0.21)(typescript@5.8.3))) tailwindcss-animate: 1.0.7(tailwindcss@3.4.17(ts-node@10.9.2(@types/node@17.0.21)(typescript@5.8.3)))
'@medusajs/ui-preset@2.9.0(tailwindcss@4.1.7)': '@medusajs/ui-preset@2.8.7(tailwindcss@4.1.7)':
dependencies: dependencies:
'@tailwindcss/forms': 0.5.10(tailwindcss@4.1.7) '@tailwindcss/forms': 0.5.10(tailwindcss@4.1.7)
tailwindcss: 4.1.7 tailwindcss: 4.1.7
tailwindcss-animate: 1.0.7(tailwindcss@4.1.7) tailwindcss-animate: 1.0.7(tailwindcss@4.1.7)
'@medusajs/ui@4.0.19(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)(typescript@5.8.3)': '@medusajs/ui@4.0.17(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)(typescript@5.8.3)':
dependencies: dependencies:
'@medusajs/icons': 2.9.0(react@19.0.0-rc-66855b96-20241106) '@medusajs/icons': 2.8.7(react@19.0.0-rc-66855b96-20241106)
'@tanstack/react-table': 8.20.5(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106) '@tanstack/react-table': 8.20.5(react-dom@19.0.0-rc-66855b96-20241106(react@19.0.0-rc-66855b96-20241106))(react@19.0.0-rc-66855b96-20241106)
clsx: 1.2.1 clsx: 1.2.1
copy-to-clipboard: 3.3.3 copy-to-clipboard: 3.3.3
@@ -11316,9 +11332,9 @@ snapshots:
- '@types/react-dom' - '@types/react-dom'
- typescript - typescript
'@medusajs/ui@4.0.19(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.8.3)': '@medusajs/ui@4.0.17(@types/react-dom@19.1.5(@types/react@19.1.4))(@types/react@19.1.4)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(typescript@5.8.3)':
dependencies: dependencies:
'@medusajs/icons': 2.9.0(react@19.1.0) '@medusajs/icons': 2.8.7(react@19.1.0)
'@tanstack/react-table': 8.20.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@tanstack/react-table': 8.20.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
clsx: 1.2.1 clsx: 1.2.1
copy-to-clipboard: 3.3.3 copy-to-clipboard: 3.3.3
@@ -17286,6 +17302,10 @@ snapshots:
'@types/node@17.0.21': {} '@types/node@17.0.21': {}
'@types/node@22.15.30':
dependencies:
undici-types: 6.21.0
'@types/node@22.15.32': '@types/node@22.15.32':
dependencies: dependencies:
undici-types: 6.21.0 undici-types: 6.21.0
@@ -22385,6 +22405,8 @@ snapshots:
tailwind-merge@2.6.0: {} tailwind-merge@2.6.0: {}
tailwind-merge@3.3.0: {}
tailwind-merge@3.3.1: {} tailwind-merge@3.3.1: {}
tailwindcss-animate@1.0.7(tailwindcss@3.4.17(ts-node@10.9.2(@types/node@17.0.21)(typescript@5.8.3))): tailwindcss-animate@1.0.7(tailwindcss@3.4.17(ts-node@10.9.2(@types/node@17.0.21)(typescript@5.8.3))):

View File

@@ -115,5 +115,6 @@
"saveAsDraft": "Save as draft", "saveAsDraft": "Save as draft",
"confirm": "Confirm", "confirm": "Confirm",
"previous": "Previous", "previous": "Previous",
"next": "Next" "next": "Next",
"invalidDataError": "Invalid data submitted"
} }

View File

@@ -30,14 +30,21 @@
"phone": "Phone", "phone": "Phone",
"email": "Email", "email": "Email",
"results": "Analysis results", "results": "Analysis results",
"feedback": "Feedback", "feedback": "Summary",
"selectJob": "Select", "selectJob": "Select",
"unselectJob": "Deselect", "unselectJob": "Unselect",
"previousResults": "Previous results ({{date}})",
"labComment": "Lab comment",
"confirmFeedbackModal": { "confirmFeedbackModal": {
"title": "Confirm publishing feedback", "title": "Confirm publishing summary",
"description": "When confirmed, the feedback will be published to the patient." "description": "When confirmed, the summary will be published to the patient."
}, },
"updateFeedbackSuccess": "Feedback updated", "error": {
"updateFeedbackLoading": "Updating feedback...", "UNKNOWN": "Something went wrong",
"updateFeedbackError": "Failed to update feedback" "JOB_ASSIGNED": "Job already selected"
} },
"updateFeedbackSuccess": "Summary updated",
"updateFeedbackLoading": "Updating summary...",
"updateFeedbackError": "Failed to update summary",
"feedbackLengthError": "Summary must be at least 10 characters"
}

View File

@@ -122,7 +122,8 @@
"weight": "Kaal", "weight": "Kaal",
"height": "Pikkus", "height": "Pikkus",
"occurance": "Toetuse sagedus", "occurance": "Toetuse sagedus",
"amount": "Summa" "amount": "Summa",
"selectDate": "Vali kuupäev"
}, },
"wallet": { "wallet": {
"balance": "Sinu MedReporti konto seis", "balance": "Sinu MedReporti konto seis",
@@ -133,5 +134,6 @@
"saveAsDraft": "Salvesta mustandina", "saveAsDraft": "Salvesta mustandina",
"confirm": "Kinnita", "confirm": "Kinnita",
"previous": "Eelmine", "previous": "Eelmine",
"next": "Järgmine" "next": "Järgmine",
} "invalidDataError": "Vigased andmed"
}

View File

@@ -30,14 +30,21 @@
"phone": "Telefon", "phone": "Telefon",
"email": "E-mail", "email": "E-mail",
"results": "Analüüside tulemused", "results": "Analüüside tulemused",
"feedback": "Tagasiside", "feedback": "Kokkuvõte",
"selectJob": "Vali", "selectJob": "Vali",
"unselectJob": "Loobu", "unselectJob": "Loobu",
"previousResults": "Eelnevad tulemused ({{date}})",
"labComment": "Labori kommentaarid",
"confirmFeedbackModal": { "confirmFeedbackModal": {
"title": "Kinnita tagasiside avaldamine", "title": "Kinnita kokkuvõtte avaldamine",
"description": "Tagasiside kinnitamisel avaldatakse see patsiendile." "description": "Kinnitamisel avaldatakse kokkuvõte patsiendile."
}, },
"updateFeedbackSuccess": "Tagasiside uuendatud", "error": {
"updateFeedbackLoading": "Tagasiside uuendatakse...", "UNKNOWN": "Midagi läks valesti",
"updateFeedbackError": "Tagasiside uuendamine ebaõnnestus" "JOB_ASSIGNED": "Töö on juba võetud"
},
"updateFeedbackSuccess": "Kokkuvõte uuendatud",
"updateFeedbackLoading": "Kokkuvõtet uuendatakse...",
"updateFeedbackError": "Kokkuvõtte uuendamine ebaõnnestus",
"feedbackLengthError": "Kokkuvõte peab olema vähemalt 10 tähemärki pikk"
} }

View File

@@ -1,7 +1,7 @@
{ {
"home": { "home": {
"pageTitle": "Ülevaade", "pageTitle": "Ülevaade",
"headerTitle": "{{companyName}} tervise ülevaade", "headerTitle": "{{companyName}} Tervisekassa kokkuvõte",
"healthDetails": "Ettevõtte terviseandmed", "healthDetails": "Ettevõtte terviseandmed",
"membersSettingsButtonTitle": "Halda töötajaid", "membersSettingsButtonTitle": "Halda töötajaid",
"membersSettingsButtonDescription": "Lisa, muuda või eemalda töötajaid.", "membersSettingsButtonDescription": "Lisa, muuda või eemalda töötajaid.",
@@ -31,13 +31,14 @@
"volume": "Eelarve maht {{volume}}" "volume": "Eelarve maht {{volume}}"
}, },
"data": { "data": {
"reservations": "{{value}} broneeringut", "reservations": "{{value}} teenust",
"analysis": "Analüüsid", "analysis": "Analüüsid",
"doctorsAndSpecialists": "Eriarstid ja spetsialistid", "doctorsAndSpecialists": "Eriarstid ja spetsialistid",
"researches": "Uuringud", "researches": "Uuringud",
"healthResearchPlans": "Terviseuuringute paketid", "healthResearchPlans": "Terviseuuringute paketid",
"serviceUsage": "{{value}} teenuse kasutust", "serviceUsage": "{{value}} teenuse kasutust",
"serviceSum": "Teenuste summa" "serviceSum": "Teenuste summa",
"eclinic": "Digikliinik"
} }
}, },
"healthDetails": { "healthDetails": {

View File

@@ -113,5 +113,6 @@
"saveAsDraft": "Save as draft", "saveAsDraft": "Save as draft",
"confirm": "Confirm", "confirm": "Confirm",
"previous": "Previous", "previous": "Previous",
"next": "Next" "next": "Next",
"invalidDataError": "Invalid data submitted"
} }

View File

@@ -30,14 +30,21 @@
"phone": "Phone", "phone": "Phone",
"email": "Email", "email": "Email",
"results": "Analysis results", "results": "Analysis results",
"feedback": "Feedback", "feedback": "Summary",
"selectJob": "Select", "selectJob": "Select",
"unselectJob": "Deselect", "unselectJob": "Unselect",
"previousResults": "Previous results ({{date}})",
"labComment": "Lab comment",
"confirmFeedbackModal": { "confirmFeedbackModal": {
"title": "Confirm publishing feedback", "title": "Confirm publishing summary",
"description": "When confirmed, the feedback will be published to the patient." "description": "When confirmed, the summary will be published to the patient."
}, },
"updateFeedbackSuccess": "Feedback updated", "error": {
"updateFeedbackLoading": "Updating feedback...", "UNKNOWN": "Something went wrong",
"updateFeedbackError": "Failed to update feedback" "JOB_ASSIGNED": "Job already selected"
},
"updateFeedbackSuccess": "Summary updated",
"updateFeedbackLoading": "Updating summary...",
"updateFeedbackError": "Failed to update summary",
"feedbackLengthError": "Summary must be at least 10 characters"
} }

View File

@@ -0,0 +1,2 @@
ALTER TABLE medreport.analysis_response_elements
ADD comment text;

View File

@@ -0,0 +1,48 @@
DROP FUNCTION IF EXISTS medreport.get_account_members(text);
CREATE OR REPLACE FUNCTION medreport.get_account_members(account_slug text)
RETURNS TABLE(
id uuid,
user_id uuid,
account_id uuid,
role character varying,
role_hierarchy_level integer,
primary_owner_user_id uuid,
name character varying,
email character varying,
personal_code text,
picture_url character varying,
created_at timestamp with time zone,
updated_at timestamp with time zone
)
LANGUAGE plpgsql
SET search_path TO ''
AS $function$begin
return QUERY
select
acc.id,
am.user_id,
am.account_id,
am.account_role,
r.hierarchy_level,
a.primary_owner_user_id,
acc.name,
acc.email,
acc.personal_code,
acc.picture_url,
am.created_at,
am.updated_at
from
medreport.accounts_memberships am
join medreport.accounts a on a.id = am.account_id
join medreport.accounts acc on acc.id = am.user_id
join medreport.roles r on r.name = am.account_role
where
a.slug = account_slug;
end;$function$
;
grant
execute on function medreport.get_account_members (text) to authenticated,
service_role;

View File

@@ -0,0 +1,85 @@
create type audit.doctor_page_view_action as ENUM('VIEW_ANALYSIS_RESULTS','VIEW_DASHBOARD','VIEW_OPEN_JOBS','VIEW_OWN_JOBS','VIEW_DONE_JOBS');
create table audit.doctor_page_views (
"id" bigint generated by default as identity not null,
"viewer_user_id" uuid references auth.users (id) not null,
"data_owner_user_id" uuid references auth.users (id),
"viewed_record_key" text,
"action" audit.doctor_page_view_action not null,
"created_at" timestamp with time zone not null default now()
);
grant usage on schema audit to authenticated;
grant select, insert, update, delete on table audit.doctor_page_views to authenticated;
alter table "audit"."page_views" enable row level security;
create policy "insert_own"
on audit.doctor_page_views
as permissive
for insert
to authenticated
with check (auth.uid() = viewer_user_id);
CREATE OR REPLACE FUNCTION medreport.log_doctor_analysis_feedback_changes()
RETURNS TRIGGER AS $$
DECLARE
current_user_id uuid;
current_user_role text;
operation_type text;
BEGIN
begin
current_user_id := auth.uid();
current_user_role := auth.jwt() ->> 'role';
end;
IF (OLD.doctor_user_id IS DISTINCT FROM NEW.doctor_user_id) THEN
operation_type := CASE
WHEN NEW.doctor_user_id IS NULL THEN 'UNSELECT_JOB'
ELSE 'SELECT_JOB'
END;
ELSIF (NEW.status = 'DRAFT' OR (OLD.status IS DISTINCT FROM NEW.status AND NEW.status = 'COMPLETED')) THEN
operation_type := CASE
WHEN NEW.status = 'DRAFT' THEN 'UPDATED_DRAFT'
WHEN NEW.status = 'COMPLETED' THEN 'PUBLISHED_SUMMARY'
ELSE NULL
END;
ELSE
operation_type := NULL;
END IF;
IF operation_type IS NOT NULL THEN
INSERT INTO audit.log_entries (
schema_name,
table_name,
record_key,
operation,
changed_by,
changed_by_role,
changed_at
)
VALUES (
'medreport',
'doctor_analysis_feedback',
NEW.id::text,
operation_type,
current_user_id,
current_user_role,
NOW()
);
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
DROP TRIGGER IF EXISTS doctor_analysis_feedback_audit_trigger ON medreport.doctor_analysis_feedback;
CREATE TRIGGER doctor_analysis_feedback_audit_trigger
AFTER UPDATE ON medreport.doctor_analysis_feedback
FOR EACH ROW
EXECUTE FUNCTION medreport.log_doctor_analysis_feedback_changes();
GRANT EXECUTE ON FUNCTION medreport.log_doctor_analysis_feedback_changes() TO authenticated;
alter table audit.doctor_page_views enable row level security;

View File

@@ -0,0 +1,42 @@
create or replace function audit.log_audit_changes()
returns trigger
language plpgsql
as $$
declare
current_user_id uuid;
current_user_role text;
begin
begin
current_user_id := auth.uid();
current_user_role := auth.jwt() ->> 'role';
end;
insert into audit.log_entries (
schema_name,
table_name,
record_key,
operation,
row_data,
changed_data,
changed_by,
changed_by_role
)
values (
tg_table_schema,
tg_table_name,
case when tg_op in ('DELETE', 'UPDATE') then
COALESCE( to_jsonb(OLD)->>'id',
to_jsonb(OLD)->>'user_id',
to_jsonb(OLD)->>'account_id' )
else null
end,
tg_op,
case when tg_op in ('DELETE', 'UPDATE') then to_jsonb(old) else null end,
case when tg_op in ('INSERT', 'UPDATE') then to_jsonb(new) else null end,
current_user_id,
current_user_role
);
return null;
end;
$$;