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 { Trans } from '@kit/ui/trans';
|
||||
|
||||
import { AppLogo } from '@kit/shared/components/app-logo';
|
||||
import appConfig from '@kit/shared/config/app.config';
|
||||
|
||||
export function SiteFooter() {
|
||||
return (
|
||||
<Footer
|
||||
|
||||
@@ -5,7 +5,7 @@ import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
||||
|
||||
import { withI18n } from '~/lib/i18n/with-i18n';
|
||||
import {
|
||||
PAGE_VIEW_ACTION,
|
||||
PageViewAction,
|
||||
createPageViewLog,
|
||||
} from '~/lib/services/audit/pageView.service';
|
||||
|
||||
@@ -23,7 +23,7 @@ async function MembershipConfirmation() {
|
||||
}
|
||||
await createPageViewLog({
|
||||
accountId: user.id,
|
||||
action: PAGE_VIEW_ACTION.REGISTRATION_SUCCESS,
|
||||
action: PageViewAction.REGISTRATION_SUCCESS,
|
||||
});
|
||||
|
||||
return <MembershipConfirmationNotification userId={user.id} />;
|
||||
|
||||
@@ -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,
|
||||
@@ -80,35 +86,41 @@ export default function AnalysisView({
|
||||
data: DoctorAnalysisFeedbackForm,
|
||||
status: 'DRAFT' | 'COMPLETED',
|
||||
) => {
|
||||
try {
|
||||
const feedbackPromise = giveFeedbackAction({
|
||||
...data,
|
||||
analysisOrderId: order.analysisOrderId,
|
||||
status,
|
||||
});
|
||||
const result = await 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" />);
|
||||
if (!result.success) {
|
||||
return 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();
|
||||
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 +131,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 +179,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 +193,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 +240,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>
|
||||
|
||||
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 { 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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -2,6 +2,12 @@ import { cache } from 'react';
|
||||
|
||||
import { getAnalysisResultsForDoctor } from '@kit/doctor/services/doctor-analysis.service';
|
||||
import { PageBody, PageHeader } from '@kit/ui/page';
|
||||
|
||||
import {
|
||||
DoctorPageViewAction,
|
||||
createDoctorPageViewLog,
|
||||
} from '~/lib/services/audit/doctorPageView.service';
|
||||
|
||||
import AnalysisView from '../../_components/analysis-view';
|
||||
import { DoctorGuard } from '../../_components/doctor-guard';
|
||||
|
||||
@@ -19,6 +25,14 @@ async function AnalysisPage({
|
||||
return null;
|
||||
}
|
||||
|
||||
if (analysisResultDetails) {
|
||||
await createDoctorPageViewLog({
|
||||
action: DoctorPageViewAction.VIEW_ANALYSIS_RESULTS,
|
||||
recordKey: id,
|
||||
dataOwnerUserId: analysisResultDetails.patient.userId,
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHeader />
|
||||
|
||||
@@ -1,10 +1,19 @@
|
||||
import { getUserDoneResponsesAction } from '@kit/doctor/actions/table-data-fetching-actions';
|
||||
import { PageBody, PageHeader } from '@kit/ui/page';
|
||||
|
||||
import { getUserDoneResponsesAction } from '@kit/doctor/actions/table-data-fetching-actions';
|
||||
import ResultsTableWrapper from '../_components/results-table-wrapper';
|
||||
import {
|
||||
DoctorPageViewAction,
|
||||
createDoctorPageViewLog,
|
||||
} from '~/lib/services/audit/doctorPageView.service';
|
||||
|
||||
import { DoctorGuard } from '../_components/doctor-guard';
|
||||
import ResultsTableWrapper from '../_components/results-table-wrapper';
|
||||
|
||||
async function CompletedJobsPage() {
|
||||
await createDoctorPageViewLog({
|
||||
action: DoctorPageViewAction.VIEW_DONE_JOBS,
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHeader />
|
||||
|
||||
@@ -1,9 +1,19 @@
|
||||
import { getUserInProgressResponsesAction } from '@kit/doctor/actions/table-data-fetching-actions';
|
||||
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 ResultsTableWrapper from '../_components/results-table-wrapper';
|
||||
|
||||
async function MyReviewsPage() {
|
||||
await createDoctorPageViewLog({
|
||||
action: DoctorPageViewAction.VIEW_OWN_JOBS,
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHeader />
|
||||
|
||||
@@ -1,10 +1,19 @@
|
||||
import { getOpenResponsesAction } from '@kit/doctor/actions/table-data-fetching-actions';
|
||||
import { PageBody, PageHeader } from '@kit/ui/page';
|
||||
|
||||
import {
|
||||
DoctorPageViewAction,
|
||||
createDoctorPageViewLog,
|
||||
} from '~/lib/services/audit/doctorPageView.service';
|
||||
|
||||
import { DoctorGuard } from '../_components/doctor-guard';
|
||||
import ResultsTableWrapper from '../_components/results-table-wrapper';
|
||||
|
||||
async function OpenJobsPage() {
|
||||
await createDoctorPageViewLog({
|
||||
action: DoctorPageViewAction.VIEW_OPEN_JOBS,
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHeader />
|
||||
|
||||
@@ -1,8 +1,18 @@
|
||||
import { PageBody, PageHeader } from '@kit/ui/page';
|
||||
|
||||
import {
|
||||
DoctorPageViewAction,
|
||||
createDoctorPageViewLog,
|
||||
} from '~/lib/services/audit/doctorPageView.service';
|
||||
|
||||
import Dashboard from './_components/doctor-dashboard';
|
||||
import { DoctorGuard } from './_components/doctor-guard';
|
||||
|
||||
async function DoctorPage() {
|
||||
await createDoctorPageViewLog({
|
||||
action: DoctorPageViewAction.VIEW_DASHBOARD,
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHeader />
|
||||
|
||||
@@ -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,11 +37,15 @@ export enum AnalysisStatus {
|
||||
const Analysis = ({
|
||||
analysisElement,
|
||||
results,
|
||||
startIcon,
|
||||
endIcon,
|
||||
isCancelled,
|
||||
}: {
|
||||
analysisElement: AnalysisElement;
|
||||
results?: UserAnalysisElement;
|
||||
analysisElement: Pick<AnalysisElement, 'analysis_name_lab'>;
|
||||
results?: AnalysisResultForDisplay;
|
||||
isCancelled?: boolean;
|
||||
startIcon?: ReactElement | null;
|
||||
endIcon?: ReactNode | null;
|
||||
}) => {
|
||||
const name = analysisElement.analysis_name_lab || '';
|
||||
const status = results?.norm_status || AnalysisStatus.NORMAL;
|
||||
@@ -78,59 +82,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" />}
|
||||
</>
|
||||
) : (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>
|
||||
{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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -13,7 +13,7 @@ import { pathsConfig } from '@kit/shared/config';
|
||||
|
||||
import { getAnalysisElements } from '~/lib/services/analysis-element.service';
|
||||
import {
|
||||
PAGE_VIEW_ACTION,
|
||||
PageViewAction,
|
||||
createPageViewLog,
|
||||
} from '~/lib/services/audit/pageView.service';
|
||||
import { AnalysisOrder, getAnalysisOrders } from '~/lib/services/order.service';
|
||||
@@ -50,7 +50,7 @@ async function AnalysisResultsPage() {
|
||||
|
||||
await createPageViewLog({
|
||||
accountId: account.id,
|
||||
action: PAGE_VIEW_ACTION.VIEW_ANALYSIS_RESULTS,
|
||||
action: PageViewAction.VIEW_ANALYSIS_RESULTS,
|
||||
});
|
||||
|
||||
const getAnalysisElementIds = (analysisOrders: AnalysisOrder[]) => [
|
||||
|
||||
@@ -8,7 +8,10 @@ import { z } from 'zod';
|
||||
|
||||
import { UserWorkspaceContextProvider } from '@kit/accounts/components';
|
||||
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 { SidebarProvider } from '@kit/ui/shadcn-sidebar';
|
||||
|
||||
@@ -92,7 +95,7 @@ function MobileNavigation({
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
<AppLogo />
|
||||
<AppLogo href={pathsConfig.app.home} />
|
||||
|
||||
<HomeMobileNavigation workspace={workspace} cart={cart} />
|
||||
</>
|
||||
|
||||
@@ -6,7 +6,7 @@ import { Trans } from '@kit/ui/trans';
|
||||
import { HomeLayoutPageHeader } from '../../_components/home-page-header';
|
||||
import { loadAnalyses } from '../../_lib/server/load-analyses';
|
||||
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';
|
||||
|
||||
export const generateMetadata = async () => {
|
||||
@@ -27,7 +27,7 @@ async function OrderAnalysisPage() {
|
||||
|
||||
await createPageViewLog({
|
||||
accountId: account.id,
|
||||
action: PAGE_VIEW_ACTION.VIEW_ORDER_ANALYSIS,
|
||||
action: PageViewAction.VIEW_ORDER_ANALYSIS,
|
||||
});
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,27 +1,35 @@
|
||||
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 { Trans } from '@kit/ui/trans';
|
||||
import { AppLogo } from '@kit/shared/components/app-logo';
|
||||
import { ProfileAccountDropdownContainer } from '@kit/shared/components/personal-account-dropdown-container';
|
||||
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 { Card } from '@kit/ui/shadcn/card';
|
||||
import { Trans } from '@kit/ui/trans';
|
||||
|
||||
import { UserNotifications } from '../_components/user-notifications';
|
||||
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 { workspace, user, accounts } = props.workspace;
|
||||
const totalValue = props.cart?.total ? formatCurrency({
|
||||
currencyCode: props.cart.currency_code,
|
||||
locale: language,
|
||||
value: props.cart.total,
|
||||
}) : 0;
|
||||
const totalValue = props.cart?.total
|
||||
? formatCurrency({
|
||||
currencyCode: props.cart.currency_code,
|
||||
locale: language,
|
||||
value: props.cart.total,
|
||||
})
|
||||
: 0;
|
||||
|
||||
const cartItemsCount = props.cart?.items?.length ?? 0;
|
||||
const hasCartItems = cartItemsCount > 0;
|
||||
@@ -29,8 +37,7 @@ export async function HomeMenuNavigation(props: { workspace: UserWorkspace, cart
|
||||
return (
|
||||
<div className={'flex w-full flex-1 items-center justify-between gap-3'}>
|
||||
<div className={`flex items-center ${SIDEBAR_WIDTH_PROPERTY}`}>
|
||||
<AppLogo />
|
||||
|
||||
<AppLogo href={pathsConfig.app.home} />
|
||||
</div>
|
||||
<Search
|
||||
className="flex grow"
|
||||
@@ -38,15 +45,27 @@ export async function HomeMenuNavigation(props: { workspace: UserWorkspace, cart
|
||||
/>
|
||||
|
||||
<div className="flex items-center justify-end gap-3">
|
||||
<Card className="px-6 py-2">
|
||||
<span>€ {Number(0).toFixed(2).replace('.', ',')}</span>
|
||||
</Card>
|
||||
{hasCartItems && (
|
||||
<Button className='relative px-4 py-2 h-10 border-1 mr-0 cursor-pointer' variant='ghost'>
|
||||
<span className='flex items-center text-nowrap'>{totalValue}</span>
|
||||
<Button
|
||||
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>
|
||||
)}
|
||||
<Link href='/home/cart'>
|
||||
<Button variant="ghost" className='relative px-4 py-2 h-10 border-1 mr-0 cursor-pointer' >
|
||||
<Link href="/home/cart">
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="relative mr-0 h-10 cursor-pointer border-1 px-4 py-2"
|
||||
>
|
||||
<ShoppingCart className="stroke-[1.5px]" />
|
||||
<Trans i18nKey="common:shoppingCartCount" values={{ count: cartItemsCount }} />
|
||||
<Trans
|
||||
i18nKey="common:shoppingCartCount"
|
||||
values={{ count: cartItemsCount }}
|
||||
/>
|
||||
</Button>
|
||||
</Link>
|
||||
<UserNotifications userId={user.id} />
|
||||
|
||||
@@ -1,29 +1,40 @@
|
||||
import React, { use } from 'react';
|
||||
import React from 'react';
|
||||
|
||||
import { redirect } from 'next/navigation';
|
||||
|
||||
import { formatCurrency } from '@/packages/shared/src/utils';
|
||||
import { Database } from '@/packages/supabase/src/database.types';
|
||||
import { PiggyBankIcon, Settings } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { createPath, pathsConfig } from '@kit/shared/config';
|
||||
import { Card, CardTitle } from '@kit/ui/card';
|
||||
import { cn } from '@kit/ui/lib/utils';
|
||||
import { Button } from '@kit/ui/shadcn/button';
|
||||
import { Trans } from '@kit/ui/trans';
|
||||
|
||||
import { pathsConfig } from '@kit/shared/config';
|
||||
|
||||
import { createPath } from '@kit/shared/config/team-account-navigation.config';
|
||||
|
||||
interface TeamAccountBenefitStatisticsProps {
|
||||
employeeCount: number;
|
||||
accountSlug: string;
|
||||
companyParams: Database['medreport']['Tables']['company_params']['Row'];
|
||||
}
|
||||
|
||||
const StatisticsCard = ({ children }: { children: React.ReactNode }) => {
|
||||
return <Card className="p-4">{children}</Card>;
|
||||
};
|
||||
|
||||
const StatisticsCardTitle = ({ children }: { children: React.ReactNode }) => {
|
||||
return <CardTitle className="text-sm font-medium">{children}</CardTitle>;
|
||||
const StatisticsCardTitle = ({
|
||||
children,
|
||||
className,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}) => {
|
||||
return (
|
||||
<CardTitle className={cn('text-sm font-medium', className)}>
|
||||
{children}
|
||||
</CardTitle>
|
||||
);
|
||||
};
|
||||
|
||||
const StatisticsDescription = ({ children }: { children: React.ReactNode }) => {
|
||||
@@ -35,7 +46,9 @@ const StatisticsValue = ({ children }: { children: React.ReactNode }) => {
|
||||
};
|
||||
|
||||
const TeamAccountBenefitStatistics = ({
|
||||
employeeCount,
|
||||
accountSlug,
|
||||
companyParams,
|
||||
}: TeamAccountBenefitStatisticsProps) => {
|
||||
const {
|
||||
i18n: { language },
|
||||
@@ -77,7 +90,8 @@ const TeamAccountBenefitStatistics = ({
|
||||
i18nKey="teams:benefitStatistics.budget.volume"
|
||||
values={{
|
||||
volume: formatCurrency({
|
||||
value: 15000,
|
||||
value:
|
||||
(Number(companyParams.benefit_amount) || 0) * employeeCount,
|
||||
locale: language,
|
||||
currencyCode: 'EUR',
|
||||
}),
|
||||
@@ -88,11 +102,17 @@ const TeamAccountBenefitStatistics = ({
|
||||
</Card>
|
||||
|
||||
<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>
|
||||
<StatisticsCardTitle>
|
||||
<Trans i18nKey="teams:benefitStatistics.data.analysis" />
|
||||
</StatisticsCardTitle>
|
||||
<StatisticsValue>18 %</StatisticsValue>
|
||||
<StatisticsValue>200 €</StatisticsValue>
|
||||
<StatisticsDescription>
|
||||
<Trans
|
||||
i18nKey="teams:benefitStatistics.data.reservations"
|
||||
@@ -104,7 +124,7 @@ const TeamAccountBenefitStatistics = ({
|
||||
<StatisticsCardTitle>
|
||||
<Trans i18nKey="teams:benefitStatistics.data.doctorsAndSpecialists" />
|
||||
</StatisticsCardTitle>
|
||||
<StatisticsValue>22 %</StatisticsValue>
|
||||
<StatisticsValue>200 €</StatisticsValue>
|
||||
<StatisticsDescription>
|
||||
<Trans
|
||||
i18nKey="teams:benefitStatistics.data.reservations"
|
||||
@@ -116,7 +136,7 @@ const TeamAccountBenefitStatistics = ({
|
||||
<StatisticsCardTitle>
|
||||
<Trans i18nKey="teams:benefitStatistics.data.researches" />
|
||||
</StatisticsCardTitle>
|
||||
<StatisticsValue>20 %</StatisticsValue>
|
||||
<StatisticsValue>200 €</StatisticsValue>
|
||||
<StatisticsDescription>
|
||||
<Trans
|
||||
i18nKey="teams:benefitStatistics.data.reservations"
|
||||
@@ -125,8 +145,10 @@ const TeamAccountBenefitStatistics = ({
|
||||
</StatisticsDescription>
|
||||
</StatisticsCard>
|
||||
<StatisticsCard>
|
||||
<StatisticsCardTitle>E-konsultatsioon</StatisticsCardTitle>
|
||||
<StatisticsValue>17 %</StatisticsValue>
|
||||
<StatisticsCardTitle>
|
||||
<Trans i18nKey="teams:benefitStatistics.data.eclinic" />
|
||||
</StatisticsCardTitle>
|
||||
<StatisticsValue>200 €</StatisticsValue>
|
||||
<StatisticsDescription>
|
||||
<Trans
|
||||
i18nKey="teams:benefitStatistics.data.reservations"
|
||||
@@ -134,28 +156,19 @@ const TeamAccountBenefitStatistics = ({
|
||||
/>
|
||||
</StatisticsDescription>
|
||||
</StatisticsCard>
|
||||
<Card className="col-span-2 p-4">
|
||||
|
||||
<StatisticsCard>
|
||||
<StatisticsCardTitle>
|
||||
<Trans i18nKey="teams:benefitStatistics.data.healthResearchPlans" />
|
||||
</StatisticsCardTitle>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<div className="border-r">
|
||||
<StatisticsValue>23 %</StatisticsValue>
|
||||
<StatisticsDescription>
|
||||
<Trans
|
||||
i18nKey="teams:benefitStatistics.data.serviceUsage"
|
||||
values={{ value: 46 }}
|
||||
/>
|
||||
</StatisticsDescription>
|
||||
</div>
|
||||
<div className="ml-4">
|
||||
<StatisticsValue>1800 €</StatisticsValue>
|
||||
<StatisticsDescription>
|
||||
<Trans i18nKey="teams:benefitStatistics.data.serviceSum" />
|
||||
</StatisticsDescription>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
<StatisticsValue>200 €</StatisticsValue>
|
||||
<StatisticsDescription>
|
||||
<Trans
|
||||
i18nKey="teams:benefitStatistics.data.serviceUsage"
|
||||
values={{ value: 46 }}
|
||||
/>
|
||||
</StatisticsDescription>
|
||||
</StatisticsCard>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,33 +1,36 @@
|
||||
import React from 'react';
|
||||
|
||||
import { Database } from '@/packages/supabase/src/database.types';
|
||||
|
||||
import { Card } from '@kit/ui/card';
|
||||
import { Trans } from '@kit/ui/trans';
|
||||
import { cn } from '@kit/ui/utils';
|
||||
|
||||
import {
|
||||
NormStatus,
|
||||
getAccountHealthDetailsFields,
|
||||
} from '../_lib/server/load-team-account-health-details';
|
||||
import { getAccountHealthDetailsFields } from '../_lib/server/load-team-account-health-details';
|
||||
import { TeamAccountStatisticsProps } from './team-account-statistics';
|
||||
|
||||
const TeamAccountHealthDetails = ({
|
||||
memberParams,
|
||||
bmiThresholds,
|
||||
members,
|
||||
}: {
|
||||
memberParams: TeamAccountStatisticsProps['memberParams'];
|
||||
bmiThresholds: Omit<
|
||||
Database['medreport']['Tables']['bmi_thresholds']['Row'],
|
||||
'id'
|
||||
>[];
|
||||
members: Database['medreport']['Functions']['get_account_members']['Returns'];
|
||||
}) => {
|
||||
const accountHealthDetailsFields =
|
||||
getAccountHealthDetailsFields(memberParams);
|
||||
const accountHealthDetailsFields = getAccountHealthDetailsFields(
|
||||
memberParams,
|
||||
bmiThresholds,
|
||||
members,
|
||||
);
|
||||
return (
|
||||
<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}>
|
||||
<div
|
||||
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,
|
||||
})}
|
||||
>
|
||||
<div className={cn('absolute top-2 right-2 rounded-2xl p-2', iconBg)}>
|
||||
<Icon color="white" className="stroke-2" />
|
||||
</div>
|
||||
<h5 className="mt-8 leading-none">
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import type { User } from '@supabase/supabase-js';
|
||||
|
||||
import { ApplicationRole } from '@kit/accounts/types/accounts';
|
||||
import { ProfileAccountDropdownContainer } from '@kit/shared/components/personal-account-dropdown-container';
|
||||
import { getTeamAccountSidebarConfig } from '@kit/shared/config';
|
||||
import {
|
||||
Sidebar,
|
||||
SidebarContent,
|
||||
@@ -8,8 +10,6 @@ import {
|
||||
SidebarHeader,
|
||||
} 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 { TeamAccountAccountsSelector } from '../_components/team-account-accounts-selector';
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import {
|
||||
getTeamAccountSidebarConfig,
|
||||
pathsConfig,
|
||||
} from '@/packages/shared/src/config';
|
||||
|
||||
import { AppLogo } from '@kit/shared/components/app-logo';
|
||||
import { ProfileAccountDropdownContainer } from '@kit/shared/components/personal-account-dropdown-container';
|
||||
import { getTeamAccountSidebarConfig } from '@kit/shared/config/team-account-navigation.config';
|
||||
|
||||
// local imports
|
||||
import { TeamAccountWorkspace } from '../_lib/server/team-account-workspace.loader';
|
||||
@@ -46,7 +50,7 @@ export function TeamAccountNavigationMenu(props: {
|
||||
return (
|
||||
<div className={'flex w-full flex-1 justify-between'}>
|
||||
<div className={'flex items-center space-x-8'}>
|
||||
<AppLogo />
|
||||
<AppLogo href={pathsConfig.app.home} />
|
||||
</div>
|
||||
|
||||
<div className={'flex items-center justify-end gap-2 space-x-2.5'}>
|
||||
|
||||
@@ -1,16 +1,25 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
|
||||
import { redirect } from 'next/navigation';
|
||||
|
||||
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 { Trans } from '@kit/ui/makerkit/trans';
|
||||
|
||||
import { pathsConfig } from '@kit/shared/config';
|
||||
|
||||
import { createPath } from '@kit/shared/config/team-account-navigation.config';
|
||||
import { Button } from '@kit/ui/shadcn/button';
|
||||
import { Calendar, DateRange } from '@kit/ui/shadcn/calendar';
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from '@kit/ui/shadcn/popover';
|
||||
|
||||
import TeamAccountBenefitStatistics from './team-account-benefit-statistics';
|
||||
import TeamAccountHealthDetails from './team-account-health-details';
|
||||
@@ -21,82 +30,145 @@ export interface TeamAccountStatisticsProps {
|
||||
Database['medreport']['Tables']['account_params']['Row'],
|
||||
'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({
|
||||
teamAccount,
|
||||
memberParams,
|
||||
bmiThresholds,
|
||||
members,
|
||||
companyParams,
|
||||
}: 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 (
|
||||
<div
|
||||
className={
|
||||
'animate-in fade-in flex flex-col space-y-4 pb-36 duration-500'
|
||||
}
|
||||
>
|
||||
<TeamAccountBenefitStatistics accountSlug={teamAccount.slug || ''} />
|
||||
<>
|
||||
<div className="mt-4 flex items-center justify-between">
|
||||
<h4 className="font-bold">
|
||||
<Trans
|
||||
i18nKey={'teams:home.headerTitle'}
|
||||
values={{ companyName: teamAccount.name }}
|
||||
/>
|
||||
</h4>
|
||||
|
||||
<h5 className="mt-4 mb-2">
|
||||
<Trans i18nKey="teams:home.healthDetails" />
|
||||
</h5>
|
||||
<div className="flex flex-col gap-2 sm:flex-row">
|
||||
<TeamAccountHealthDetails memberParams={memberParams} />
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button variant="outline" data-empty={!date}>
|
||||
<CalendarIcon />
|
||||
{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">
|
||||
<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={
|
||||
'animate-in fade-in flex flex-col space-y-4 pb-36 duration-500'
|
||||
}
|
||||
>
|
||||
<TeamAccountBenefitStatistics
|
||||
employeeCount={members.length}
|
||||
accountSlug={teamAccount.slug || ''}
|
||||
companyParams={companyParams}
|
||||
/>
|
||||
|
||||
<h5 className="mt-4 mb-2">
|
||||
<Trans i18nKey="teams:home.healthDetails" />
|
||||
</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">
|
||||
<ChevronRight className="stroke-2" />
|
||||
</div>
|
||||
<div className="bg-primary/10 w-fit rounded-2xl p-2">
|
||||
<User color="green" className="stroke-2" />
|
||||
<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.membersSettingsButtonTitle" />
|
||||
<Trans i18nKey="teams:home.membersBillingButtonTitle" />
|
||||
</span>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
<Trans i18nKey="teams:home.membersSettingsButtonDescription" />
|
||||
<Trans i18nKey="teams:home.membersBillingButtonDescription" />
|
||||
</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">
|
||||
<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>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
import React from 'react';
|
||||
|
||||
import { Database } from '@/packages/supabase/src/database.types';
|
||||
import Isikukood from 'isikukood';
|
||||
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';
|
||||
|
||||
@@ -14,66 +20,89 @@ interface AccountHealthDetailsField {
|
||||
color?: string;
|
||||
className?: string;
|
||||
}>;
|
||||
normStatus: NormStatus;
|
||||
}
|
||||
|
||||
export enum NormStatus {
|
||||
CRITICAL = 'CRITICAL',
|
||||
WARNING = 'WARNING',
|
||||
NORMAL = 'NORMAL',
|
||||
iconBg: string;
|
||||
}
|
||||
|
||||
export const getAccountHealthDetailsFields = (
|
||||
memberParams: TeamAccountStatisticsProps['memberParams'],
|
||||
bmiThresholds: Omit<
|
||||
Database['medreport']['Tables']['bmi_thresholds']['Row'],
|
||||
'id'
|
||||
>[],
|
||||
members: Database['medreport']['Functions']['get_account_members']['Returns'],
|
||||
): AccountHealthDetailsField[] => {
|
||||
const averageBMI = (
|
||||
memberParams.reduce((sum, { height, weight }) => {
|
||||
return bmiFromMetric(weight ?? 0, height ?? 0) + sum;
|
||||
}, 0) / memberParams.length
|
||||
).toFixed(0);
|
||||
const avarageWeight =
|
||||
memberParams.reduce((sum, r) => sum + r.weight!, 0) / memberParams.length;
|
||||
const avarageHeight =
|
||||
memberParams.reduce((sum, r) => sum + r.height!, 0) / memberParams.length;
|
||||
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 [
|
||||
{
|
||||
title: 'teams:healthDetails.women',
|
||||
value: `50% (${memberParams.length})`,
|
||||
value: `${femalePercentage}% (${numberOfFemaleMembers})`,
|
||||
Icon: User,
|
||||
normStatus: NormStatus.NORMAL,
|
||||
iconBg: 'bg-success',
|
||||
},
|
||||
{
|
||||
title: 'teams:healthDetails.men',
|
||||
value: `50% (${memberParams.length})`,
|
||||
value: `${malePercentage}% (${numberOfMaleMembers})`,
|
||||
Icon: User,
|
||||
normStatus: NormStatus.NORMAL,
|
||||
iconBg: 'bg-success',
|
||||
},
|
||||
{
|
||||
title: 'teams:healthDetails.avgAge',
|
||||
value: '56',
|
||||
value: avarageAge.toFixed(0),
|
||||
Icon: Clock,
|
||||
normStatus: NormStatus.NORMAL,
|
||||
iconBg: 'bg-success',
|
||||
},
|
||||
{
|
||||
title: 'teams:healthDetails.bmi',
|
||||
value: averageBMI,
|
||||
Icon: TrendingUp,
|
||||
normStatus: NormStatus.WARNING,
|
||||
iconBg: getBmiBackgroundColor(bmiStatus),
|
||||
},
|
||||
{
|
||||
title: 'teams:healthDetails.cholesterol',
|
||||
value: '6.1',
|
||||
value: '-',
|
||||
Icon: TrendingUp,
|
||||
normStatus: NormStatus.WARNING,
|
||||
iconBg: 'bg-warning',
|
||||
},
|
||||
{
|
||||
title: 'teams:healthDetails.vitaminD',
|
||||
value: '76',
|
||||
value: '-',
|
||||
Icon: TrendingUp,
|
||||
normStatus: NormStatus.NORMAL,
|
||||
iconBg: 'bg-warning',
|
||||
},
|
||||
{
|
||||
title: 'teams:healthDetails.smokers',
|
||||
value: '22%',
|
||||
value: '-',
|
||||
Icon: TrendingUp,
|
||||
normStatus: NormStatus.CRITICAL,
|
||||
iconBg: 'bg-warning',
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
@@ -8,16 +8,12 @@ import { z } from 'zod';
|
||||
|
||||
import { LineItemSchema } from '@kit/billing';
|
||||
import { getBillingGatewayProvider } from '@kit/billing-gateway';
|
||||
import { appConfig, billingConfig, pathsConfig } from '@kit/shared/config';
|
||||
import { getLogger } from '@kit/shared/logger';
|
||||
import { requireUser } from '@kit/supabase/require-user';
|
||||
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
||||
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';
|
||||
|
||||
export function createTeamBillingService(client: SupabaseClient<Database>) {
|
||||
|
||||
@@ -5,19 +5,21 @@ import { cookies } from 'next/headers';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { AppLogo } from '@kit/shared/components/app-logo';
|
||||
import { getTeamAccountSidebarConfig } from '@kit/shared/config';
|
||||
import { getTeamAccountSidebarConfig, pathsConfig } from '@kit/shared/config';
|
||||
import {
|
||||
CompanyGuard,
|
||||
TeamAccountWorkspaceContextProvider,
|
||||
} from '@kit/team-accounts/components';
|
||||
import { Page, PageMobileNavigation, PageNavigation } from '@kit/ui/page';
|
||||
import { SidebarProvider } from '@kit/ui/shadcn-sidebar';
|
||||
|
||||
import { withI18n } from '~/lib/i18n/with-i18n';
|
||||
|
||||
// local imports
|
||||
import { TeamAccountLayoutMobileNavigation } from './_components/team-account-layout-mobile-navigation';
|
||||
import { TeamAccountLayoutSidebar } from './_components/team-account-layout-sidebar';
|
||||
import { TeamAccountNavigationMenu } from './_components/team-account-navigation-menu';
|
||||
import { loadTeamWorkspace } from './_lib/server/team-account-workspace.loader';
|
||||
import { withI18n } from '~/lib/i18n/with-i18n';
|
||||
|
||||
type TeamWorkspaceLayoutProps = React.PropsWithChildren<{
|
||||
params: Promise<{ account: string }>;
|
||||
@@ -66,7 +68,7 @@ function SidebarLayout({
|
||||
</PageNavigation>
|
||||
|
||||
<PageMobileNavigation className={'flex items-center justify-between'}>
|
||||
<AppLogo />
|
||||
<AppLogo href={pathsConfig.app.home} />
|
||||
|
||||
<div className={'flex space-x-4'}>
|
||||
<TeamAccountLayoutMobileNavigation
|
||||
@@ -109,7 +111,7 @@ function HeaderLayout({
|
||||
</PageNavigation>
|
||||
|
||||
<PageMobileNavigation className={'flex items-center justify-between'}>
|
||||
<AppLogo />
|
||||
<AppLogo href={pathsConfig.app.home} />
|
||||
|
||||
<div className={'group-data-[mobile:hidden]'}>
|
||||
<TeamAccountLayoutMobileNavigation
|
||||
@@ -134,7 +136,7 @@ function HeaderLayout({
|
||||
<PageMobileNavigation
|
||||
className={'flex items-center justify-between'}
|
||||
>
|
||||
<AppLogo />
|
||||
<AppLogo href={pathsConfig.app.home} />
|
||||
<div className={'flex space-x-4'}>
|
||||
<TeamAccountLayoutMobileNavigation
|
||||
userId={data.user.id}
|
||||
|
||||
@@ -2,18 +2,21 @@
|
||||
|
||||
import { use } from 'react';
|
||||
|
||||
import { createAccountsApi } from '@/packages/features/accounts/src/server/api';
|
||||
import { CompanyGuard } from '@/packages/features/team-accounts/src/components';
|
||||
import { createTeamAccountsApi } from '@/packages/features/team-accounts/src/server/api';
|
||||
import { getSupabaseServerClient } from '@/packages/supabase/src/clients/server-client';
|
||||
|
||||
import { PageBody } from '@kit/ui/page';
|
||||
import { Trans } from '@kit/ui/trans';
|
||||
|
||||
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
|
||||
import { withI18n } from '~/lib/i18n/with-i18n';
|
||||
import {
|
||||
PageViewAction,
|
||||
createPageViewLog,
|
||||
} from '~/lib/services/audit/pageView.service';
|
||||
|
||||
import { Dashboard } from './_components/dashboard';
|
||||
import { TeamAccountLayoutPageHeader } from './_components/team-account-layout-page-header';
|
||||
import { withI18n } from '~/lib/i18n/with-i18n';
|
||||
|
||||
interface TeamAccountHomePageProps {
|
||||
params: Promise<{ account: string }>;
|
||||
@@ -31,25 +34,32 @@ export const generateMetadata = async () => {
|
||||
function TeamAccountHomePage({ params }: TeamAccountHomePageProps) {
|
||||
const account = use(params).account;
|
||||
const client = getSupabaseServerClient();
|
||||
const api = createTeamAccountsApi(client);
|
||||
const teamAccount = use(api.getTeamAccount(account));
|
||||
const { memberParams } = use(api.getMembers(account));
|
||||
const teamAccountsApi = createTeamAccountsApi(client);
|
||||
const accountsApi = createAccountsApi(client);
|
||||
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 (
|
||||
<>
|
||||
<TeamAccountLayoutPageHeader
|
||||
title={
|
||||
<Trans
|
||||
i18nKey={'teams:home.headerTitle'}
|
||||
values={{ companyName: account }}
|
||||
/>
|
||||
}
|
||||
<PageBody>
|
||||
<Dashboard
|
||||
teamAccount={teamAccount}
|
||||
memberParams={memberParams}
|
||||
bmiThresholds={bmiThresholds}
|
||||
members={members}
|
||||
companyParams={companyParams}
|
||||
/>
|
||||
|
||||
<PageBody>
|
||||
<Dashboard teamAccount={teamAccount} memberParams={memberParams} />
|
||||
</PageBody>
|
||||
</>
|
||||
</PageBody>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { MetadataRoute } from 'next';
|
||||
|
||||
import appConfig from '@kit/shared/config/app.config';
|
||||
import { appConfig } from '@kit/shared/config';
|
||||
|
||||
export default function robots(): MetadataRoute.Robots {
|
||||
return {
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
import Link from 'next/link';
|
||||
|
||||
import SelectAnalysisPackages from '@kit/shared/components/select-analysis-packages';
|
||||
import { CaretRightIcon } from '@radix-ui/react-icons';
|
||||
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 { Trans } from '@kit/ui/trans';
|
||||
|
||||
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
|
||||
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 { loadAnalysisPackages } from '../home/(user)/_lib/server/load-analysis-packages';
|
||||
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { getServerSideSitemap } from 'next-sitemap';
|
||||
|
||||
import { createCmsClient } from '@kit/cms';
|
||||
|
||||
import appConfig from '@kit/shared/config/app.config';
|
||||
import { appConfig } from '@kit/shared/config';
|
||||
|
||||
/**
|
||||
* @description The maximum age of the sitemap in seconds.
|
||||
|
||||
Reference in New Issue
Block a user