Merge branch 'main' into MED-57

This commit is contained in:
Danel Kungla
2025-08-26 11:30:34 +03:00
22 changed files with 507 additions and 225 deletions

View File

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

View File

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

View File

@@ -0,0 +1,108 @@
'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 { 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('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={cn('w-16', className)}
size={size}
variant="destructive"
onClick={handleUnselectJob}
disabled={isPending}
>
{isPending ? (
<LoaderCircle className="h-4 w-4 animate-spin" />
) : (
<Trans i18nKey="doctor:unselectJob" />
)}
</Button>
);
}
if (!doctorUserId) {
return (
<Button
className={cn('w-16', className)}
size={size}
onClick={handleSelectJob}
disabled={isPending}
>
{isPending ? (
<LoaderCircle className="h-4 w-4 animate-spin" />
) : (
<Trans i18nKey="doctor:selectJob" />
)}
</Button>
);
}
return <>{doctorName}</>;
}

View File

@@ -6,6 +6,9 @@ import { usePathname } from 'next/navigation';
import { UserWorkspace } from '@/app/home/(user)/_lib/server/load-user-workspace';
import { 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 {
Sidebar,
SidebarContent,
@@ -20,11 +23,6 @@ import {
} from '@kit/ui/shadcn-sidebar';
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({
accounts,
}: {
@@ -75,10 +73,7 @@ export function DoctorSidebar({
isActive={path === pathsConfig.app.myJobs}
asChild
>
<Link
className={'flex gap-2.5'}
href={pathsConfig.app.myJobs}
>
<Link className={'flex gap-2.5'} href={pathsConfig.app.myJobs}>
<LayoutDashboard className={'h-4'} />
<Trans i18nKey={'doctor:sidebar.myReviews'} />
</Link>

View File

@@ -3,15 +3,10 @@
import { useTransition } from 'react';
import Link from 'next/link';
import { useRouter } from 'next/navigation';
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 { ResponseTable } from '@kit/doctor/schema/doctor-analysis.schema';
import { pathsConfig } from '@kit/shared/config';
@@ -28,94 +23,7 @@ import {
TableRow,
} from '@kit/ui/table';
import { Trans } from '@kit/ui/trans';
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}</>;
}
import DoctorJobSelect from './doctor-job-select';
export default function ResultsTable({
results = [],
@@ -272,7 +180,7 @@ export default function ResultsTable({
/>
</TableCell>
<TableCell>
<DoctorCell
<DoctorJobSelect
doctorUserId={result.doctor?.primary_owner_user_id}
doctorName={getFullName(
result.doctor?.name,

View File

@@ -2,6 +2,7 @@ import { cache } from 'react';
import { getAnalysisResultsForDoctor } from '@kit/doctor/services/doctor-analysis.service';
import { PageBody, PageHeader } from '@kit/ui/page';
import AnalysisView from '../../_components/analysis-view';
import { DoctorGuard } from '../../_components/doctor-guard';

View File

@@ -1,6 +1,6 @@
'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 { format } from 'date-fns';
@@ -37,9 +37,13 @@ export enum AnalysisStatus {
const Analysis = ({
analysisElement,
results,
startIcon,
endIcon,
}: {
analysisElement: Pick<AnalysisElement, 'analysis_name_lab'>;
results?: AnalysisResultForDisplay;
startIcon?: ReactElement | null;
endIcon?: ReactNode | null;
}) => {
const name = analysisElement.analysis_name_lab || '';
const status = results?.norm_status || AnalysisStatus.NORMAL;
@@ -76,59 +80,66 @@ const Analysis = ({
}, [results, value, normLower]);
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="flex items-center gap-2 font-semibold">
{name}
{results?.response_time && (
<div
className="group/tooltip relative"
onClick={() => setShowTooltip(!showTooltip)}
onMouseLeave={() => setShowTooltip(false)}
>
<Info className="hover" />{' '}
<div className="border-border rounded-lg border px-5">
<div className="flex flex-col items-center justify-between gap-2 py-3 sm:h-[65px] sm:flex-row sm:gap-0">
<div className="flex items-center gap-2 font-semibold">
{startIcon || <div className="w-4" />}
{name}
{results?.response_time && (
<div
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 },
)}
className="group/tooltip relative"
onClick={(e) => {
e.stopPropagation();
setShowTooltip(!showTooltip);
}}
onMouseLeave={() => setShowTooltip(false)}
>
<Trans i18nKey="analysis-results:analysisDate" />
{': '}
{format(new Date(results.response_time), 'dd.MM.yyyy HH:mm')}
<Info className="hover" />{' '}
<div
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>
{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" />}
</>
) : (
<>
<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>
{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!}
/>
</>
) : (
<>
<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>
);
};

View File

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

View File

@@ -35,6 +35,7 @@ export const selectJobAction = doctorAction(
logger.info({ analysisOrderId }, `Successfully selected`);
revalidateDoctorAnalysis();
return { success: true };
},
{
@@ -60,7 +61,7 @@ export const unselectJobAction = doctorAction(
{ analysisOrderId },
`Successfully removed current doctor from job`,
);
revalidateDoctorAnalysis();
return { success: true };
},
{
@@ -101,6 +102,7 @@ export const giveFeedbackAction = doctorAction(
);
logger.info({ analysisOrderId }, `Successfully submitted feedback`);
revalidateDoctorAnalysis();
return result;
},
{
@@ -108,3 +110,7 @@ export const giveFeedbackAction = doctorAction(
},
),
);
function revalidateDoctorAnalysis() {
revalidatePath('/doctor/analysis');
}

View File

@@ -66,6 +66,27 @@ export const AnalysisResponseSchema = z.object({
updated_at: z.string().nullable(),
analysis_name: z.string().nullable(),
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>;

View File

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

View File

@@ -1,5 +1,7 @@
import 'server-only';
import { isBefore } from 'date-fns';
import { getSupabaseServerClient } from '@kit/supabase/server-client';
import { AnalysisResultDetails } from '../schema/doctor-analysis-detail-view.schema';
@@ -360,7 +362,7 @@ export async function getAnalysisResultsForDoctor(
): Promise<AnalysisResultDetails> {
const supabase = getSupabaseServerClient();
const { data: analysisResponse, error } = await supabase
const { data: analysisResponseElements, error } = await supabase
.schema('medreport')
.from(`analysis_response_elements`)
.select(
@@ -373,20 +375,26 @@ export async function getAnalysisResultsForDoctor(
throw new Error('Something went wrong.');
}
const firstAnalysisResponse = analysisResponse?.[0];
const firstAnalysisResponse = analysisResponseElements?.[0];
const userId = firstAnalysisResponse?.analysis_responses.user_id;
const medusaOrderId =
firstAnalysisResponse?.analysis_responses?.analysis_order_id
?.medusa_order_id;
if (!analysisResponse?.length || !userId || !medusaOrderId) {
if (!analysisResponseElements?.length || !userId || !medusaOrderId) {
throw new Error('Failed to retrieve full analysis data.');
}
const responseElementAnalysisElementOriginalIds =
analysisResponseElements.map(
({ analysis_element_original_id }) => analysis_element_original_id,
);
const [
{ data: medusaOrderItems, error: medusaOrderError },
{ data: accountWithParams, error: accountError },
{ data: doctorFeedback, error: feedbackError },
{ data: previousAnalyses, error: previousAnalysesError },
] = await Promise.all([
supabase
.schema('public')
@@ -403,7 +411,7 @@ export async function getAnalysisResultsForDoctor(
.eq('is_personal_account', true)
.eq('primary_owner_user_id', userId)
.limit(1),
await supabase
supabase
.schema('medreport')
.from('doctor_analysis_feedback')
.select(`*`)
@@ -412,9 +420,39 @@ export async function getAnalysisResultsForDoctor(
firstAnalysisResponse.analysis_responses.analysis_order_id.id,
)
.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.');
}
@@ -433,8 +471,25 @@ export async function getAnalysisResultsForDoctor(
account_params,
} = 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 {
analysisResponse,
analysisResponse: analysisResponseElementsWithPreviousData,
order: {
title: medusaOrderItems?.[0]?.item_id.product_title ?? '-',
isPackage:

View File

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

View File

@@ -549,6 +549,7 @@ export type Database = {
analysis_element_original_id: string
analysis_name: string | null
analysis_response_id: number
comment: string | null
created_at: string
id: number
norm_lower: number | null
@@ -566,6 +567,7 @@ export type Database = {
analysis_element_original_id: string
analysis_name?: string | null
analysis_response_id: number
comment?: string | null
created_at?: string
id?: number
norm_lower?: number | null
@@ -583,6 +585,7 @@ export type Database = {
analysis_element_original_id?: string
analysis_name?: string | null
analysis_response_id?: number
comment?: string | null
created_at?: string
id?: number
norm_lower?: number | null
@@ -1723,7 +1726,9 @@ export type Database = {
Returns: Json
}
create_team_account: {
Args: { account_name: string; new_personal_code: string }
Args:
| { account_name: string }
| { account_name: string; new_personal_code: string }
Returns: {
application_role: Database["medreport"]["Enums"]["application_role"]
city: string | null
@@ -1911,6 +1916,22 @@ export type Database = {
}
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: {
Args: {
target_account_id: string

View File

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

View File

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

View File

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

View File

@@ -134,5 +134,6 @@
"saveAsDraft": "Salvesta mustandina",
"confirm": "Kinnita",
"previous": "Eelmine",
"next": "Järgmine"
"next": "Järgmine",
"invalidDataError": "Vigased andmed"
}

View File

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

View File

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

View File

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

View File

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