Merge branch 'main' into MED-85
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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} />;
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
103
app/doctor/_components/doctor-analysis-wrapper.tsx
Normal file
103
app/doctor/_components/doctor-analysis-wrapper.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
120
app/doctor/_components/doctor-job-select.tsx
Normal file
120
app/doctor/_components/doctor-job-select.tsx
Normal 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}</>;
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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 />
|
||||||
|
|||||||
@@ -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 />
|
||||||
|
|||||||
@@ -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 />
|
||||||
|
|||||||
@@ -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 />
|
||||||
|
|||||||
@@ -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 />
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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[]) => [
|
||||||
|
|||||||
@@ -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} />
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
|||||||
@@ -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} />
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
@@ -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'}>
|
||||||
|
|||||||
@@ -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>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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>) {
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
46
lib/services/audit/doctorPageView.service.ts
Normal file
46
lib/services/audit/doctorPageView.service.ts
Normal 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);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -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();
|
||||||
|
|||||||
@@ -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
|
||||||
})),
|
})),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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');
|
||||||
|
}
|
||||||
|
|||||||
@@ -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>;
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
>;
|
>;
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
export enum ErrorReason {
|
||||||
|
JOB_ASSIGNED = 'JOB_ASSIGNED',
|
||||||
|
UNKNOWN = 'UNKNOWN',
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -291,6 +291,7 @@ export class TeamAccountsApi {
|
|||||||
.single();
|
.single();
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
|
console.warn('Error fetching company params', error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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"],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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
130
pnpm-lock.yaml
generated
@@ -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))):
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
@@ -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": {
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
@@ -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"
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE medreport.analysis_response_elements
|
||||||
|
ADD comment text;
|
||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
42
supabase/migrations/20250826160800_improve_logging.sql
Normal file
42
supabase/migrations/20250826160800_improve_logging.sql
Normal 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;
|
||||||
|
$$;
|
||||||
|
|
||||||
Reference in New Issue
Block a user