MED-137: add doctor other jobs view (#55)
* add doctor jobs view * change translation * another translation change * clean up * add analaysis detail view to paths config * translation * merge fix * fix path * move components to shared * refactor * imports * clean up
This commit is contained in:
242
app/doctor/_components/analysis-view.tsx
Normal file
242
app/doctor/_components/analysis-view.tsx
Normal file
@@ -0,0 +1,242 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { useForm } from 'react-hook-form';
|
||||
|
||||
import { giveFeedbackAction } from '@kit/doctor/actions/doctor-server-actions';
|
||||
import {
|
||||
getDOBWithAgeStringFromPersonalCode,
|
||||
getResultSetName,
|
||||
} from '@kit/doctor/lib/helpers';
|
||||
import {
|
||||
AnalysisResponse,
|
||||
DoctorFeedback,
|
||||
Order,
|
||||
Patient,
|
||||
} from '@kit/doctor/schema/doctor-analysis-detail-view.schema';
|
||||
import {
|
||||
DoctorAnalysisFeedbackForm,
|
||||
doctorAnalysisFeedbackSchema,
|
||||
} from '@kit/doctor/schema/doctor-analysis.schema';
|
||||
import ConfirmationModal from '@kit/shared/components/confirmation-modal';
|
||||
import { getFullName } from '@kit/shared/utils';
|
||||
import { useUser } from '@kit/supabase/hooks/use-user';
|
||||
import { Button } from '@kit/ui/button';
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormMessage,
|
||||
} from '@kit/ui/form';
|
||||
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';
|
||||
|
||||
export default function AnalysisView({
|
||||
patient,
|
||||
order,
|
||||
analyses,
|
||||
feedback,
|
||||
}: {
|
||||
patient: Patient;
|
||||
order: Order;
|
||||
analyses: AnalysisResponse[];
|
||||
feedback?: DoctorFeedback;
|
||||
}) {
|
||||
const [isConfirmOpen, setIsConfirmOpen] = useState(false);
|
||||
|
||||
const { data: user } = useUser();
|
||||
|
||||
const isInProgress =
|
||||
!!feedback?.status &&
|
||||
feedback?.doctor_user_id &&
|
||||
feedback?.status !== 'COMPLETED';
|
||||
const isReadOnly =
|
||||
!isInProgress ||
|
||||
(!!feedback?.doctor_user_id && feedback?.doctor_user_id !== user?.id);
|
||||
|
||||
const form = useForm({
|
||||
resolver: zodResolver(doctorAnalysisFeedbackSchema),
|
||||
defaultValues: {
|
||||
feedbackValue: feedback?.value ?? '',
|
||||
userId: patient.userId,
|
||||
},
|
||||
});
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
if (!patient || !order || !analyses) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const onSubmit = async (
|
||||
data: DoctorAnalysisFeedbackForm,
|
||||
status: 'DRAFT' | 'COMPLETED',
|
||||
) => {
|
||||
try {
|
||||
const feedbackPromise = giveFeedbackAction({
|
||||
...data,
|
||||
analysisOrderId: order.analysisOrderId,
|
||||
status,
|
||||
});
|
||||
|
||||
toast.promise(() => feedbackPromise, {
|
||||
success: <Trans i18nKey={'doctor:updateFeedbackSuccess'} />,
|
||||
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" />);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDraftSubmit = () => {
|
||||
const formData = form.getValues();
|
||||
onSubmit(formData, 'DRAFT');
|
||||
};
|
||||
|
||||
const handleCompleteSubmit = () => {
|
||||
setIsConfirmOpen(true);
|
||||
};
|
||||
|
||||
const confirmComplete = () => {
|
||||
const formData = form.getValues();
|
||||
onSubmit(formData, 'COMPLETED');
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<h3>
|
||||
<Trans
|
||||
i18nKey={getResultSetName(
|
||||
order.title,
|
||||
order.isPackage,
|
||||
Object.keys(analyses)?.length,
|
||||
)}
|
||||
/>
|
||||
</h3>
|
||||
<div className="grid grid-cols-2">
|
||||
<div className="font-bold">
|
||||
<Trans i18nKey="doctor:name" />
|
||||
</div>
|
||||
<div>{getFullName(patient.firstName, patient.lastName)}</div>
|
||||
<div className="font-bold">
|
||||
<Trans i18nKey="doctor:personalCode" />
|
||||
</div>
|
||||
<div>{patient.personalCode ?? ''}</div>
|
||||
<div className="font-bold">
|
||||
<Trans i18nKey="doctor:dobAndAge" />
|
||||
</div>
|
||||
<div>{getDOBWithAgeStringFromPersonalCode(patient.personalCode)}</div>
|
||||
<div className="font-bold">
|
||||
<Trans i18nKey="doctor:height" />
|
||||
</div>
|
||||
<div>{patient.height}</div>
|
||||
<div className="font-bold">
|
||||
<Trans i18nKey="doctor:weight" />
|
||||
</div>
|
||||
<div>{patient.weight}</div>
|
||||
<div className="font-bold">
|
||||
<Trans i18nKey="doctor:bmi" />
|
||||
</div>
|
||||
<div>{bmiFromMetric(patient?.height ?? 0, patient?.weight ?? 0)}</div>
|
||||
<div className="font-bold">
|
||||
<Trans i18nKey="doctor:smoking" />
|
||||
</div>
|
||||
<div></div>
|
||||
<div className="font-bold">
|
||||
<Trans i18nKey="doctor:phone" />
|
||||
</div>
|
||||
<div>{patient.phone}</div>
|
||||
<div className="font-bold">
|
||||
<Trans i18nKey="doctor:email" />
|
||||
</div>
|
||||
<div>{patient.email}</div>
|
||||
</div>
|
||||
|
||||
<h3>
|
||||
<Trans i18nKey="doctor:results" />
|
||||
</h3>
|
||||
<div className="flex flex-col gap-2">
|
||||
{analyses.map((analysisData) => {
|
||||
return (
|
||||
<Analysis
|
||||
key={analysisData.id}
|
||||
analysisElement={{
|
||||
analysis_name_lab: analysisData.analysis_name,
|
||||
}}
|
||||
results={analysisData}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<h3>
|
||||
<Trans i18nKey="doctor:feedback" />
|
||||
</h3>
|
||||
|
||||
<p>{feedback?.value ?? '-'}</p>
|
||||
|
||||
{!isReadOnly && (
|
||||
<Form {...form}>
|
||||
<form className="space-y-4 lg:w-1/2">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="feedbackValue"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Textarea {...field} disabled={isReadOnly} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
handleDraftSubmit();
|
||||
}}
|
||||
disabled={isReadOnly}
|
||||
>
|
||||
<Trans i18nKey="common:saveAsDraft" />
|
||||
</Button>
|
||||
<Button
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
handleCompleteSubmit();
|
||||
}}
|
||||
disabled={isReadOnly}
|
||||
>
|
||||
<Trans i18nKey="common:save" />
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
)}
|
||||
<ConfirmationModal
|
||||
isOpen={isConfirmOpen}
|
||||
onClose={() => setIsConfirmOpen(false)}
|
||||
onConfirm={confirmComplete}
|
||||
titleKey="doctor:confirmFeedbackModal.title"
|
||||
descriptionKey="doctor:confirmFeedbackModal.description"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
36
app/doctor/_components/doctor-dashboard.tsx
Normal file
36
app/doctor/_components/doctor-dashboard.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
'use client';
|
||||
|
||||
import {
|
||||
getOpenResponsesAction,
|
||||
getOtherResponsesAction,
|
||||
getUserDoneResponsesAction,
|
||||
getUserInProgressResponsesAction,
|
||||
} from '@kit/doctor/actions/table-data-fetching-actions';
|
||||
import ResultsTableWrapper from './results-table-wrapper';
|
||||
|
||||
export default function Dashboard() {
|
||||
return (
|
||||
<>
|
||||
<ResultsTableWrapper
|
||||
titleKey="doctor:openReviews"
|
||||
action={getOpenResponsesAction}
|
||||
queryKey="doctor-open-jobs"
|
||||
/>
|
||||
<ResultsTableWrapper
|
||||
titleKey="doctor:myReviews"
|
||||
action={getUserInProgressResponsesAction}
|
||||
queryKey="doctor-in-progress-jobs"
|
||||
/>
|
||||
<ResultsTableWrapper
|
||||
titleKey="doctor:completedReviews"
|
||||
action={getUserDoneResponsesAction}
|
||||
queryKey="doctor-done-jobs"
|
||||
/>
|
||||
<ResultsTableWrapper
|
||||
titleKey="doctor:otherReviews"
|
||||
action={getOtherResponsesAction}
|
||||
queryKey="doctor-other-jobs"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
28
app/doctor/_components/doctor-guard.tsx
Normal file
28
app/doctor/_components/doctor-guard.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import { notFound } from 'next/navigation';
|
||||
|
||||
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
||||
import { isDoctor } from '@kit/doctor/lib/server/utils/is-doctor';
|
||||
|
||||
|
||||
type LayoutOrPageComponent<Params> = React.ComponentType<Params>;
|
||||
|
||||
/**
|
||||
* DoctorGuard is a server component wrapper that checks if the user is a doctor before rendering the component.
|
||||
* If the user is not a doctor, we redirect to a 404.
|
||||
* @param Component - The Page or Layout component to wrap
|
||||
*/
|
||||
export function DoctorGuard<Params extends object>(
|
||||
Component: LayoutOrPageComponent<Params>,
|
||||
) {
|
||||
return async function DoctorGuardServerComponentWrapper(params: Params) {
|
||||
const client = getSupabaseServerClient();
|
||||
const isUserDoctor = await isDoctor(client);
|
||||
|
||||
// if the user is not a super-admin, we redirect to a 404
|
||||
if (!isUserDoctor) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
return <Component {...params} />;
|
||||
};
|
||||
}
|
||||
@@ -20,8 +20,10 @@ import {
|
||||
} from '@kit/ui/shadcn-sidebar';
|
||||
import { Trans } from '@kit/ui/trans';
|
||||
|
||||
import { AppLogo } from '~/components/app-logo';
|
||||
import { ProfileAccountDropdownContainer } from '~/components/personal-account-dropdown-container';
|
||||
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,
|
||||
@@ -33,7 +35,11 @@ export function DoctorSidebar({
|
||||
return (
|
||||
<Sidebar collapsible="icon">
|
||||
<SidebarHeader className={'m-2'}>
|
||||
<AppLogo href={'/doctor'} className="max-w-full" compact={!open} />
|
||||
<AppLogo
|
||||
href={pathsConfig.app.doctor}
|
||||
className="max-w-full"
|
||||
compact={!open}
|
||||
/>
|
||||
</SidebarHeader>
|
||||
|
||||
<SidebarContent>
|
||||
@@ -44,10 +50,49 @@ export function DoctorSidebar({
|
||||
|
||||
<SidebarGroupContent>
|
||||
<SidebarMenu>
|
||||
<SidebarMenuButton isActive={path === '/doctor'} asChild>
|
||||
<Link className={'flex gap-2.5'} href={'/doctor'}>
|
||||
<SidebarMenuButton
|
||||
isActive={path === pathsConfig.app.doctor}
|
||||
asChild
|
||||
>
|
||||
<Link className={'flex gap-2.5'} href={pathsConfig.app.doctor}>
|
||||
<LayoutDashboard className={'h-4'} />
|
||||
<span>Dashboard</span>
|
||||
<Trans i18nKey={'doctor:sidebar.dashboard'} />
|
||||
</Link>
|
||||
</SidebarMenuButton>
|
||||
<SidebarMenuButton
|
||||
isActive={path === pathsConfig.app.openJobs}
|
||||
asChild
|
||||
>
|
||||
<Link
|
||||
className={'flex gap-2.5'}
|
||||
href={pathsConfig.app.openJobs}
|
||||
>
|
||||
<LayoutDashboard className={'h-4'} />
|
||||
<Trans i18nKey={'doctor:sidebar.openReviews'} />
|
||||
</Link>
|
||||
</SidebarMenuButton>
|
||||
<SidebarMenuButton
|
||||
isActive={path === pathsConfig.app.myJobs}
|
||||
asChild
|
||||
>
|
||||
<Link
|
||||
className={'flex gap-2.5'}
|
||||
href={pathsConfig.app.myJobs}
|
||||
>
|
||||
<LayoutDashboard className={'h-4'} />
|
||||
<Trans i18nKey={'doctor:sidebar.myReviews'} />
|
||||
</Link>
|
||||
</SidebarMenuButton>
|
||||
<SidebarMenuButton
|
||||
isActive={path === pathsConfig.app.completedJobs}
|
||||
asChild
|
||||
>
|
||||
<Link
|
||||
className={'flex gap-2.5'}
|
||||
href={pathsConfig.app.completedJobs}
|
||||
>
|
||||
<LayoutDashboard className={'h-4'} />
|
||||
<Trans i18nKey={'doctor:sidebar.completedReviews'} />
|
||||
</Link>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenu>
|
||||
|
||||
@@ -2,13 +2,14 @@ import Link from 'next/link';
|
||||
|
||||
import { Menu } from 'lucide-react';
|
||||
|
||||
import { pathsConfig } from '@kit/shared/config';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from '@kit/ui/dropdown-menu';
|
||||
import pathsConfig from '../../../config/paths.config';
|
||||
import { Trans } from '@kit/ui/trans';
|
||||
|
||||
export function DoctorMobileNavigation() {
|
||||
return (
|
||||
@@ -19,9 +20,30 @@ export function DoctorMobileNavigation() {
|
||||
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuItem>
|
||||
<Link href={pathsConfig.app.doctor}>Home</Link>
|
||||
<Link href={pathsConfig.app.home}>
|
||||
<Trans i18nKey={'common:routes.home'} />
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<Link href={pathsConfig.app.doctor}>
|
||||
<Trans i18nKey={'doctor:sidebar.dashboard'} />
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<Link href={pathsConfig.app.openJobs}>
|
||||
<Trans i18nKey={'doctor:sidebar.openReviews'} />
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<Link href={pathsConfig.app.myJobs}>
|
||||
<Trans i18nKey={'doctor:sidebar.myReviews'} />
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<Link href={pathsConfig.app.completedJobs}>
|
||||
<Trans i18nKey={'doctor:sidebar.completedReviews'} />
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
|
||||
120
app/doctor/_components/results-table-wrapper.tsx
Normal file
120
app/doctor/_components/results-table-wrapper.tsx
Normal file
@@ -0,0 +1,120 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
|
||||
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
|
||||
import {
|
||||
PaginatedData,
|
||||
ResponseTable,
|
||||
ServerActionResponse,
|
||||
} from '@kit/doctor/schema/doctor-analysis.schema';
|
||||
import TableSkeleton from '@kit/shared/components/table-skeleton';
|
||||
import { Trans } from '@kit/ui/trans';
|
||||
import { cn } from '@kit/ui/utils';
|
||||
|
||||
import ResultsTable from './results-table';
|
||||
|
||||
const PAGE_SIZE = 10;
|
||||
|
||||
export default function ResultsTableWrapper({
|
||||
titleKey,
|
||||
action,
|
||||
queryKey,
|
||||
}: {
|
||||
titleKey: string;
|
||||
action: ({
|
||||
page,
|
||||
pageSize,
|
||||
}: {
|
||||
page: number;
|
||||
pageSize: number;
|
||||
}) => Promise<ServerActionResponse<PaginatedData<ResponseTable>>>;
|
||||
queryKey: string;
|
||||
}) {
|
||||
const [page, setPage] = useState(1);
|
||||
|
||||
const {
|
||||
data: jobs,
|
||||
isLoading,
|
||||
isError,
|
||||
isFetching,
|
||||
} = useQuery({
|
||||
queryKey: [queryKey, 'doctor-jobs', page],
|
||||
queryFn: async () => await action({ page: page, pageSize: PAGE_SIZE }),
|
||||
placeholderData: (previousData) => previousData,
|
||||
});
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const onJobUpdate = () => {
|
||||
queryClient.invalidateQueries({
|
||||
predicate: (query) => query.queryKey.includes('doctor-jobs'),
|
||||
});
|
||||
};
|
||||
|
||||
const createPageChangeHandler = (setPage: (page: number) => void) => {
|
||||
return async ({ page }: { page: number; pageSize: number }) => {
|
||||
setPage(page);
|
||||
return { success: true, data: null };
|
||||
};
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<>
|
||||
<h3>
|
||||
<Trans i18nKey={titleKey} />
|
||||
</h3>
|
||||
|
||||
<div className="relative">
|
||||
<div
|
||||
className={`transition-opacity duration-200 ${
|
||||
isFetching ? 'opacity-50' : 'opacity-100'
|
||||
}`}
|
||||
>
|
||||
<TableSkeleton />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (isError) {
|
||||
return (
|
||||
<>
|
||||
<h3>
|
||||
<Trans i18nKey={titleKey} />
|
||||
</h3>
|
||||
<div className="flex items-center justify-center p-8">
|
||||
<div className="text-lg text-red-600">
|
||||
<Trans i18nKey="common:genericServerError" />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<h3>
|
||||
<Trans i18nKey={titleKey} />
|
||||
</h3>
|
||||
|
||||
<div className="relative">
|
||||
<div
|
||||
className={cn('opacity-100 transition-opacity duration-200', {
|
||||
'opacity-50': isFetching,
|
||||
})}
|
||||
>
|
||||
<ResultsTable
|
||||
results={jobs?.data?.data || []}
|
||||
pagination={jobs?.data?.pagination}
|
||||
fetchAction={createPageChangeHandler(setPage)}
|
||||
onJobUpdate={onJobUpdate}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
322
app/doctor/_components/results-table.tsx
Normal file
322
app/doctor/_components/results-table.tsx
Normal file
@@ -0,0 +1,322 @@
|
||||
'use client';
|
||||
|
||||
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 {
|
||||
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';
|
||||
import { getFullName } from '@kit/shared/utils';
|
||||
import { useUser } from '@kit/supabase/hooks/use-user';
|
||||
import { Button } from '@kit/ui/button';
|
||||
import { toast } from '@kit/ui/sonner';
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
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}</>;
|
||||
}
|
||||
|
||||
export default function ResultsTable({
|
||||
results = [],
|
||||
pagination = {
|
||||
currentPage: 1,
|
||||
totalPages: 1,
|
||||
totalCount: 0,
|
||||
pageSize: 10,
|
||||
},
|
||||
fetchAction,
|
||||
onJobUpdate,
|
||||
}: {
|
||||
results: ResponseTable[] | null;
|
||||
pagination?: {
|
||||
currentPage: number;
|
||||
totalPages: number;
|
||||
totalCount: number;
|
||||
pageSize: number;
|
||||
};
|
||||
fetchAction: ({
|
||||
page,
|
||||
pageSize,
|
||||
}: {
|
||||
page: number;
|
||||
pageSize: number;
|
||||
}) => Promise<{
|
||||
success: boolean;
|
||||
data: null;
|
||||
}>;
|
||||
onJobUpdate: () => void;
|
||||
}) {
|
||||
const [isPending, startTransition] = useTransition();
|
||||
const { data: currentUser } = useUser();
|
||||
|
||||
const fetchPage = async (page: number) => {
|
||||
startTransition(async () => {
|
||||
const result = await fetchAction({
|
||||
page,
|
||||
pageSize: pagination.pageSize,
|
||||
});
|
||||
if (!result.success) {
|
||||
toast.error('common.genericServerError');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleNextPage = () => {
|
||||
if (pagination.currentPage < pagination.totalPages) {
|
||||
fetchPage(pagination.currentPage + 1);
|
||||
}
|
||||
};
|
||||
|
||||
const handlePrevPage = () => {
|
||||
if (pagination.currentPage > 1) {
|
||||
fetchPage(pagination.currentPage - 1);
|
||||
}
|
||||
};
|
||||
|
||||
const handleJobUpdate = () => {
|
||||
onJobUpdate();
|
||||
fetchPage(pagination.currentPage);
|
||||
};
|
||||
|
||||
if (!results?.length) {
|
||||
return (
|
||||
<p>
|
||||
<Trans i18nKey="common:noData" />.
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Table className="border-separate rounded-lg border">
|
||||
<TableHeader className="text-ui-fg-subtle txt-medium-plus">
|
||||
<TableRow>
|
||||
<TableHead className="w-6"></TableHead>
|
||||
<TableHead className="w-20">
|
||||
<Trans i18nKey="doctor:resultsTable.patientName" />
|
||||
</TableHead>
|
||||
<TableHead className="w-20">
|
||||
<Trans i18nKey="doctor:resultsTable.serviceName" />
|
||||
</TableHead>
|
||||
<TableHead className="w-20">
|
||||
<Trans i18nKey="doctor:resultsTable.orderNr" />
|
||||
</TableHead>
|
||||
<TableHead className="w-20">
|
||||
<Trans i18nKey="doctor:resultsTable.time" />
|
||||
</TableHead>
|
||||
<TableHead className="w-20">
|
||||
<Trans i18nKey="doctor:resultsTable.resultsStatus" />
|
||||
</TableHead>
|
||||
<TableHead className="w-20">
|
||||
<Trans i18nKey="doctor:resultsTable.assignedTo" />
|
||||
</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{results
|
||||
?.sort((a, b) =>
|
||||
(a.created_at ?? '') > (b.created_at ?? '') ? -1 : 1,
|
||||
)
|
||||
.map((result) => {
|
||||
const isCompleted = result.feedback?.status === 'COMPLETED';
|
||||
const isCurrentDoctorJob =
|
||||
!!result.doctor?.primary_owner_user_id &&
|
||||
result.doctor?.primary_owner_user_id === currentUser?.id;
|
||||
|
||||
const resultsReceived = result.elements.length || 0;
|
||||
const elementsInOrder = Array.from(
|
||||
new Set(result.analysis_order_id.analysis_element_ids),
|
||||
)?.length;
|
||||
|
||||
return (
|
||||
<TableRow key={result.order_number}>
|
||||
<TableCell className="text-center">
|
||||
<Link
|
||||
href={`/${pathsConfig.app.analysisDetails}/${result.id}`}
|
||||
>
|
||||
<Eye />
|
||||
</Link>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{getFullName(
|
||||
result.patient?.name,
|
||||
result.patient?.last_name,
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Trans
|
||||
i18nKey={getResultSetName(
|
||||
result.order?.title ?? '-',
|
||||
result.order?.isPackage,
|
||||
result.elements?.length || 0,
|
||||
)}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>{result.order_number}</TableCell>
|
||||
<TableCell>
|
||||
{result.firstSampleGivenAt
|
||||
? format(result.firstSampleGivenAt, 'dd.MM.yyyy HH:mm')
|
||||
: '-'}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Trans
|
||||
i18nKey={
|
||||
resultsReceived === elementsInOrder
|
||||
? 'doctor:resultsTable.responsesReceived'
|
||||
: 'doctor:resultsTable.waitingForNr'
|
||||
}
|
||||
values={{
|
||||
nr: elementsInOrder - resultsReceived,
|
||||
}}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<DoctorCell
|
||||
doctorUserId={result.doctor?.primary_owner_user_id}
|
||||
doctorName={getFullName(
|
||||
result.doctor?.name,
|
||||
result.doctor?.last_name,
|
||||
)}
|
||||
analysisOrderId={result.analysis_order_id?.id}
|
||||
userId={result.patient?.id}
|
||||
isRemovable={!isCompleted && isCurrentDoctorJob}
|
||||
onJobUpdate={handleJobUpdate}
|
||||
linkTo={`/${pathsConfig.app.analysisDetails}/${result.id}`}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<div className="mt-4 flex items-center justify-between">
|
||||
<Button
|
||||
onClick={handlePrevPage}
|
||||
disabled={pagination.currentPage === 1 || isPending}
|
||||
variant="outline"
|
||||
>
|
||||
<Trans i18nKey="common:previous" />
|
||||
</Button>
|
||||
<span className="text-sm text-gray-600">
|
||||
<Trans
|
||||
i18nKey={'common:pageOfPages'}
|
||||
values={{
|
||||
page: pagination.currentPage,
|
||||
total: pagination.totalPages,
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
<Button
|
||||
onClick={handleNextPage}
|
||||
disabled={
|
||||
pagination.currentPage === pagination.totalPages || isPending
|
||||
}
|
||||
variant="outline"
|
||||
>
|
||||
<Trans i18nKey="common:next" />
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user