feat(team-accounts): enhance team account statistics and health details components with new data and improved calculations
This commit is contained in:
@@ -3,10 +3,12 @@ import React, { use } from 'react';
|
|||||||
import { redirect } from 'next/navigation';
|
import { redirect } from 'next/navigation';
|
||||||
|
|
||||||
import { formatCurrency } from '@/packages/shared/src/utils';
|
import { formatCurrency } from '@/packages/shared/src/utils';
|
||||||
|
import { Database } from '@/packages/supabase/src/database.types';
|
||||||
import { PiggyBankIcon, Settings } from 'lucide-react';
|
import { PiggyBankIcon, Settings } from 'lucide-react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { Card, CardTitle } from '@kit/ui/card';
|
import { Card, CardTitle } from '@kit/ui/card';
|
||||||
|
import { cn } from '@kit/ui/lib/utils';
|
||||||
import { Button } from '@kit/ui/shadcn/button';
|
import { Button } from '@kit/ui/shadcn/button';
|
||||||
import { Trans } from '@kit/ui/trans';
|
import { Trans } from '@kit/ui/trans';
|
||||||
|
|
||||||
@@ -14,15 +16,27 @@ import pathsConfig from '~/config/paths.config';
|
|||||||
import { createPath } from '~/config/team-account-navigation.config';
|
import { createPath } from '~/config/team-account-navigation.config';
|
||||||
|
|
||||||
interface TeamAccountBenefitStatisticsProps {
|
interface TeamAccountBenefitStatisticsProps {
|
||||||
|
employeeCount: number;
|
||||||
accountSlug: string;
|
accountSlug: string;
|
||||||
|
companyParams: Database['medreport']['Tables']['company_params']['Row'];
|
||||||
}
|
}
|
||||||
|
|
||||||
const StatisticsCard = ({ children }: { children: React.ReactNode }) => {
|
const StatisticsCard = ({ children }: { children: React.ReactNode }) => {
|
||||||
return <Card className="p-4">{children}</Card>;
|
return <Card className="p-4">{children}</Card>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const StatisticsCardTitle = ({ children }: { children: React.ReactNode }) => {
|
const StatisticsCardTitle = ({
|
||||||
return <CardTitle className="text-sm font-medium">{children}</CardTitle>;
|
children,
|
||||||
|
className,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode;
|
||||||
|
className?: string;
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<CardTitle className={cn('text-sm font-medium', className)}>
|
||||||
|
{children}
|
||||||
|
</CardTitle>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const StatisticsDescription = ({ children }: { children: React.ReactNode }) => {
|
const StatisticsDescription = ({ children }: { children: React.ReactNode }) => {
|
||||||
@@ -34,7 +48,9 @@ const StatisticsValue = ({ children }: { children: React.ReactNode }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const TeamAccountBenefitStatistics = ({
|
const TeamAccountBenefitStatistics = ({
|
||||||
|
employeeCount,
|
||||||
accountSlug,
|
accountSlug,
|
||||||
|
companyParams,
|
||||||
}: TeamAccountBenefitStatisticsProps) => {
|
}: TeamAccountBenefitStatisticsProps) => {
|
||||||
const {
|
const {
|
||||||
i18n: { language },
|
i18n: { language },
|
||||||
@@ -76,7 +92,8 @@ const TeamAccountBenefitStatistics = ({
|
|||||||
i18nKey="teams:benefitStatistics.budget.volume"
|
i18nKey="teams:benefitStatistics.budget.volume"
|
||||||
values={{
|
values={{
|
||||||
volume: formatCurrency({
|
volume: formatCurrency({
|
||||||
value: 15000,
|
value:
|
||||||
|
(Number(companyParams.benefit_amount) || 0) * employeeCount,
|
||||||
locale: language,
|
locale: language,
|
||||||
currencyCode: 'EUR',
|
currencyCode: 'EUR',
|
||||||
}),
|
}),
|
||||||
@@ -87,11 +104,17 @@ const TeamAccountBenefitStatistics = ({
|
|||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<div className="grid flex-2 grid-cols-2 gap-2 sm:grid-cols-3 sm:grid-rows-2">
|
<div className="grid flex-2 grid-cols-2 gap-2 sm:grid-cols-3 sm:grid-rows-2">
|
||||||
|
<StatisticsCard>
|
||||||
|
<StatisticsCardTitle className="text-lg font-bold">
|
||||||
|
<Trans i18nKey="teams:benefitStatistics.data.serviceSum" />
|
||||||
|
</StatisticsCardTitle>
|
||||||
|
<StatisticsValue>1800 €</StatisticsValue>
|
||||||
|
</StatisticsCard>
|
||||||
<StatisticsCard>
|
<StatisticsCard>
|
||||||
<StatisticsCardTitle>
|
<StatisticsCardTitle>
|
||||||
<Trans i18nKey="teams:benefitStatistics.data.analysis" />
|
<Trans i18nKey="teams:benefitStatistics.data.analysis" />
|
||||||
</StatisticsCardTitle>
|
</StatisticsCardTitle>
|
||||||
<StatisticsValue>18 %</StatisticsValue>
|
<StatisticsValue>200 €</StatisticsValue>
|
||||||
<StatisticsDescription>
|
<StatisticsDescription>
|
||||||
<Trans
|
<Trans
|
||||||
i18nKey="teams:benefitStatistics.data.reservations"
|
i18nKey="teams:benefitStatistics.data.reservations"
|
||||||
@@ -103,7 +126,7 @@ const TeamAccountBenefitStatistics = ({
|
|||||||
<StatisticsCardTitle>
|
<StatisticsCardTitle>
|
||||||
<Trans i18nKey="teams:benefitStatistics.data.doctorsAndSpecialists" />
|
<Trans i18nKey="teams:benefitStatistics.data.doctorsAndSpecialists" />
|
||||||
</StatisticsCardTitle>
|
</StatisticsCardTitle>
|
||||||
<StatisticsValue>22 %</StatisticsValue>
|
<StatisticsValue>200 €</StatisticsValue>
|
||||||
<StatisticsDescription>
|
<StatisticsDescription>
|
||||||
<Trans
|
<Trans
|
||||||
i18nKey="teams:benefitStatistics.data.reservations"
|
i18nKey="teams:benefitStatistics.data.reservations"
|
||||||
@@ -115,7 +138,7 @@ const TeamAccountBenefitStatistics = ({
|
|||||||
<StatisticsCardTitle>
|
<StatisticsCardTitle>
|
||||||
<Trans i18nKey="teams:benefitStatistics.data.researches" />
|
<Trans i18nKey="teams:benefitStatistics.data.researches" />
|
||||||
</StatisticsCardTitle>
|
</StatisticsCardTitle>
|
||||||
<StatisticsValue>20 %</StatisticsValue>
|
<StatisticsValue>200 €</StatisticsValue>
|
||||||
<StatisticsDescription>
|
<StatisticsDescription>
|
||||||
<Trans
|
<Trans
|
||||||
i18nKey="teams:benefitStatistics.data.reservations"
|
i18nKey="teams:benefitStatistics.data.reservations"
|
||||||
@@ -124,8 +147,10 @@ const TeamAccountBenefitStatistics = ({
|
|||||||
</StatisticsDescription>
|
</StatisticsDescription>
|
||||||
</StatisticsCard>
|
</StatisticsCard>
|
||||||
<StatisticsCard>
|
<StatisticsCard>
|
||||||
<StatisticsCardTitle>E-konsultatsioon</StatisticsCardTitle>
|
<StatisticsCardTitle>
|
||||||
<StatisticsValue>17 %</StatisticsValue>
|
<Trans i18nKey="teams:benefitStatistics.data.eclinic" />
|
||||||
|
</StatisticsCardTitle>
|
||||||
|
<StatisticsValue>200 €</StatisticsValue>
|
||||||
<StatisticsDescription>
|
<StatisticsDescription>
|
||||||
<Trans
|
<Trans
|
||||||
i18nKey="teams:benefitStatistics.data.reservations"
|
i18nKey="teams:benefitStatistics.data.reservations"
|
||||||
@@ -133,28 +158,19 @@ const TeamAccountBenefitStatistics = ({
|
|||||||
/>
|
/>
|
||||||
</StatisticsDescription>
|
</StatisticsDescription>
|
||||||
</StatisticsCard>
|
</StatisticsCard>
|
||||||
<Card className="col-span-2 p-4">
|
|
||||||
|
<StatisticsCard>
|
||||||
<StatisticsCardTitle>
|
<StatisticsCardTitle>
|
||||||
<Trans i18nKey="teams:benefitStatistics.data.healthResearchPlans" />
|
<Trans i18nKey="teams:benefitStatistics.data.healthResearchPlans" />
|
||||||
</StatisticsCardTitle>
|
</StatisticsCardTitle>
|
||||||
<div className="grid grid-cols-2 gap-2">
|
<StatisticsValue>200 €</StatisticsValue>
|
||||||
<div className="border-r">
|
<StatisticsDescription>
|
||||||
<StatisticsValue>23 %</StatisticsValue>
|
<Trans
|
||||||
<StatisticsDescription>
|
i18nKey="teams:benefitStatistics.data.serviceUsage"
|
||||||
<Trans
|
values={{ value: 46 }}
|
||||||
i18nKey="teams:benefitStatistics.data.serviceUsage"
|
/>
|
||||||
values={{ value: 46 }}
|
</StatisticsDescription>
|
||||||
/>
|
</StatisticsCard>
|
||||||
</StatisticsDescription>
|
|
||||||
</div>
|
|
||||||
<div className="ml-4">
|
|
||||||
<StatisticsValue>1800 €</StatisticsValue>
|
|
||||||
<StatisticsDescription>
|
|
||||||
<Trans i18nKey="teams:benefitStatistics.data.serviceSum" />
|
|
||||||
</StatisticsDescription>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,33 +1,36 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
|
import { Database } from '@/packages/supabase/src/database.types';
|
||||||
|
|
||||||
import { Card } from '@kit/ui/card';
|
import { Card } from '@kit/ui/card';
|
||||||
import { Trans } from '@kit/ui/trans';
|
import { Trans } from '@kit/ui/trans';
|
||||||
import { cn } from '@kit/ui/utils';
|
import { cn } from '@kit/ui/utils';
|
||||||
|
|
||||||
import {
|
import { getAccountHealthDetailsFields } from '../_lib/server/load-team-account-health-details';
|
||||||
NormStatus,
|
|
||||||
getAccountHealthDetailsFields,
|
|
||||||
} from '../_lib/server/load-team-account-health-details';
|
|
||||||
import { TeamAccountStatisticsProps } from './team-account-statistics';
|
import { TeamAccountStatisticsProps } from './team-account-statistics';
|
||||||
|
|
||||||
const TeamAccountHealthDetails = ({
|
const TeamAccountHealthDetails = ({
|
||||||
memberParams,
|
memberParams,
|
||||||
|
bmiThresholds,
|
||||||
|
members,
|
||||||
}: {
|
}: {
|
||||||
memberParams: TeamAccountStatisticsProps['memberParams'];
|
memberParams: TeamAccountStatisticsProps['memberParams'];
|
||||||
|
bmiThresholds: Omit<
|
||||||
|
Database['medreport']['Tables']['bmi_thresholds']['Row'],
|
||||||
|
'id'
|
||||||
|
>[];
|
||||||
|
members: Database['medreport']['Functions']['get_account_members']['Returns'];
|
||||||
}) => {
|
}) => {
|
||||||
const accountHealthDetailsFields =
|
const accountHealthDetailsFields = getAccountHealthDetailsFields(
|
||||||
getAccountHealthDetailsFields(memberParams);
|
memberParams,
|
||||||
|
bmiThresholds,
|
||||||
|
members,
|
||||||
|
);
|
||||||
return (
|
return (
|
||||||
<div className="grid flex-1 grid-cols-2 gap-4 md:grid-cols-3">
|
<div className="grid flex-1 grid-cols-2 gap-4 md:grid-cols-3">
|
||||||
{accountHealthDetailsFields.map(({ title, Icon, value, normStatus }) => (
|
{accountHealthDetailsFields.map(({ title, Icon, value, iconBg }) => (
|
||||||
<Card className="relative p-4" key={title}>
|
<Card className="relative p-4" key={title}>
|
||||||
<div
|
<div className={cn('absolute top-2 right-2 rounded-2xl p-2', iconBg)}>
|
||||||
className={cn('absolute top-2 right-2 rounded-2xl p-2', {
|
|
||||||
'bg-success': normStatus === NormStatus.NORMAL,
|
|
||||||
'bg-warning': normStatus === NormStatus.WARNING,
|
|
||||||
'bg-destructive': normStatus === NormStatus.CRITICAL,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<Icon color="white" className="stroke-2" />
|
<Icon color="white" className="stroke-2" />
|
||||||
</div>
|
</div>
|
||||||
<h5 className="mt-8 leading-none">
|
<h5 className="mt-8 leading-none">
|
||||||
|
|||||||
@@ -1,12 +1,24 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
import { redirect } from 'next/navigation';
|
import { redirect } from 'next/navigation';
|
||||||
|
|
||||||
import { Database } from '@/packages/supabase/src/database.types';
|
import { Database } from '@/packages/supabase/src/database.types';
|
||||||
import { ChevronRight, Euro, User } from 'lucide-react';
|
import { format } from 'date-fns';
|
||||||
|
import { enGB, et } from 'date-fns/locale';
|
||||||
|
import { CalendarIcon, ChevronRight, Euro, User } from 'lucide-react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { Card } from '@kit/ui/card';
|
import { Card } from '@kit/ui/card';
|
||||||
import { Trans } from '@kit/ui/makerkit/trans';
|
import { Trans } from '@kit/ui/makerkit/trans';
|
||||||
|
import { Button } from '@kit/ui/shadcn/button';
|
||||||
|
import { Calendar, DateRange } from '@kit/ui/shadcn/calendar';
|
||||||
|
import {
|
||||||
|
Popover,
|
||||||
|
PopoverContent,
|
||||||
|
PopoverTrigger,
|
||||||
|
} from '@kit/ui/shadcn/popover';
|
||||||
|
|
||||||
import pathsConfig from '~/config/paths.config';
|
import pathsConfig from '~/config/paths.config';
|
||||||
import { createPath } from '~/config/team-account-navigation.config';
|
import { createPath } from '~/config/team-account-navigation.config';
|
||||||
@@ -20,82 +32,145 @@ export interface TeamAccountStatisticsProps {
|
|||||||
Database['medreport']['Tables']['account_params']['Row'],
|
Database['medreport']['Tables']['account_params']['Row'],
|
||||||
'weight' | 'height'
|
'weight' | 'height'
|
||||||
>[];
|
>[];
|
||||||
|
bmiThresholds: Omit<
|
||||||
|
Database['medreport']['Tables']['bmi_thresholds']['Row'],
|
||||||
|
'id'
|
||||||
|
>[];
|
||||||
|
members: Database['medreport']['Functions']['get_account_members']['Returns'];
|
||||||
|
companyParams: Database['medreport']['Tables']['company_params']['Row'];
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function TeamAccountStatistics({
|
export default function TeamAccountStatistics({
|
||||||
teamAccount,
|
teamAccount,
|
||||||
memberParams,
|
memberParams,
|
||||||
|
bmiThresholds,
|
||||||
|
members,
|
||||||
|
companyParams,
|
||||||
}: TeamAccountStatisticsProps) {
|
}: TeamAccountStatisticsProps) {
|
||||||
|
const [date, setDate] = useState<DateRange | undefined>({
|
||||||
|
from: new Date(),
|
||||||
|
to: new Date(),
|
||||||
|
});
|
||||||
|
const {
|
||||||
|
i18n: { language },
|
||||||
|
} = useTranslation();
|
||||||
|
const dateFormatOptions = {
|
||||||
|
locale: language === 'et' ? et : enGB,
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<>
|
||||||
className={
|
<div className="mt-4 flex items-center justify-between">
|
||||||
'animate-in fade-in flex flex-col space-y-4 pb-36 duration-500'
|
<h4 className="font-bold">
|
||||||
}
|
<Trans
|
||||||
>
|
i18nKey={'teams:home.headerTitle'}
|
||||||
<TeamAccountBenefitStatistics accountSlug={teamAccount.slug || ''} />
|
values={{ companyName: teamAccount.name }}
|
||||||
|
/>
|
||||||
|
</h4>
|
||||||
|
|
||||||
<h5 className="mt-4 mb-2">
|
<Popover>
|
||||||
<Trans i18nKey="teams:home.healthDetails" />
|
<PopoverTrigger asChild>
|
||||||
</h5>
|
<Button variant="outline" data-empty={!date}>
|
||||||
<div className="flex flex-col gap-2 sm:flex-row">
|
<CalendarIcon />
|
||||||
<TeamAccountHealthDetails memberParams={memberParams} />
|
{date?.from && date?.to ? (
|
||||||
|
`${format(date.from, 'd MMMM yyyy', dateFormatOptions)} - ${format(date.to, 'd MMMM yyyy', dateFormatOptions)}`
|
||||||
|
) : (
|
||||||
|
<span>
|
||||||
|
<Trans i18nKey="common:formField.date" />
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent className="w-auto p-0">
|
||||||
|
<Calendar
|
||||||
|
mode="range"
|
||||||
|
selected={date}
|
||||||
|
onSelect={setDate}
|
||||||
|
locale={language === 'et' ? et : enGB}
|
||||||
|
/>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col gap-2">
|
<div
|
||||||
<Card
|
className={
|
||||||
variant="gradient-success"
|
'animate-in fade-in flex flex-col space-y-4 pb-36 duration-500'
|
||||||
className="border-success/50 hover:bg-success/20 relative flex h-full cursor-pointer flex-col justify-center px-6 py-4 transition-colors"
|
}
|
||||||
onClick={() =>
|
>
|
||||||
redirect(
|
<TeamAccountBenefitStatistics
|
||||||
createPath(
|
employeeCount={members.length}
|
||||||
pathsConfig.app.accountMembers,
|
accountSlug={teamAccount.slug || ''}
|
||||||
teamAccount.slug || '',
|
companyParams={companyParams}
|
||||||
),
|
/>
|
||||||
)
|
|
||||||
}
|
<h5 className="mt-4 mb-2">
|
||||||
>
|
<Trans i18nKey="teams:home.healthDetails" />
|
||||||
<div className="flex flex-col">
|
</h5>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-2 sm:flex-row">
|
||||||
|
<TeamAccountHealthDetails
|
||||||
|
memberParams={memberParams}
|
||||||
|
bmiThresholds={bmiThresholds}
|
||||||
|
members={members}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<Card
|
||||||
|
variant="gradient-success"
|
||||||
|
className="border-success/50 hover:bg-success/20 relative flex h-full cursor-pointer flex-col justify-center px-6 py-4 transition-colors"
|
||||||
|
onClick={() =>
|
||||||
|
redirect(
|
||||||
|
createPath(
|
||||||
|
pathsConfig.app.accountMembers,
|
||||||
|
teamAccount.slug || '',
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<div className="border-input absolute top-2 right-2 rounded-md border bg-white p-3">
|
||||||
|
<ChevronRight className="stroke-2" />
|
||||||
|
</div>
|
||||||
|
<div className="bg-primary/10 w-fit rounded-2xl p-2">
|
||||||
|
<User color="green" className="stroke-2" />
|
||||||
|
</div>
|
||||||
|
<span className="mt-4 mb-2 text-lg font-semibold">
|
||||||
|
<Trans i18nKey="teams:home.membersSettingsButtonTitle" />
|
||||||
|
</span>
|
||||||
|
<p className="text-muted-foreground text-sm">
|
||||||
|
<Trans i18nKey="teams:home.membersSettingsButtonDescription" />
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card
|
||||||
|
variant="gradient-warning"
|
||||||
|
className="border-warning/40 hover:bg-warning/20 relative flex h-full cursor-pointer flex-col justify-center px-6 py-4 transition-colors"
|
||||||
|
onClick={() =>
|
||||||
|
redirect(
|
||||||
|
createPath(
|
||||||
|
pathsConfig.app.accountBilling,
|
||||||
|
teamAccount.slug || '',
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
<div className="border-input absolute top-2 right-2 rounded-md border bg-white p-3">
|
<div className="border-input absolute top-2 right-2 rounded-md border bg-white p-3">
|
||||||
<ChevronRight className="stroke-2" />
|
<ChevronRight className="stroke-2" />
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-primary/10 w-fit rounded-2xl p-2">
|
<div className="bg-warning/10 w-fit rounded-2xl p-2">
|
||||||
<User color="green" className="stroke-2" />
|
<Euro color="orange" className="stroke-2" />
|
||||||
</div>
|
</div>
|
||||||
<span className="mt-4 mb-2 text-lg font-semibold">
|
<span className="mt-4 mb-2 text-lg font-semibold">
|
||||||
<Trans i18nKey="teams:home.membersSettingsButtonTitle" />
|
<Trans i18nKey="teams:home.membersBillingButtonTitle" />
|
||||||
</span>
|
</span>
|
||||||
<p className="text-muted-foreground text-sm">
|
<p className="text-muted-foreground text-sm">
|
||||||
<Trans i18nKey="teams:home.membersSettingsButtonDescription" />
|
<Trans i18nKey="teams:home.membersBillingButtonDescription" />
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</Card>
|
||||||
</Card>
|
</div>
|
||||||
|
|
||||||
<Card
|
|
||||||
variant="gradient-warning"
|
|
||||||
className="border-warning/40 hover:bg-warning/20 relative flex h-full cursor-pointer flex-col justify-center px-6 py-4 transition-colors"
|
|
||||||
onClick={() =>
|
|
||||||
redirect(
|
|
||||||
createPath(
|
|
||||||
pathsConfig.app.accountBilling,
|
|
||||||
teamAccount.slug || '',
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<div className="border-input absolute top-2 right-2 rounded-md border bg-white p-3">
|
|
||||||
<ChevronRight className="stroke-2" />
|
|
||||||
</div>
|
|
||||||
<div className="bg-warning/10 w-fit rounded-2xl p-2">
|
|
||||||
<Euro color="orange" className="stroke-2" />
|
|
||||||
</div>
|
|
||||||
<span className="mt-4 mb-2 text-lg font-semibold">
|
|
||||||
<Trans i18nKey="teams:home.membersBillingButtonTitle" />
|
|
||||||
</span>
|
|
||||||
<p className="text-muted-foreground text-sm">
|
|
||||||
<Trans i18nKey="teams:home.membersBillingButtonDescription" />
|
|
||||||
</p>
|
|
||||||
</Card>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,14 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
|
import { Database } from '@/packages/supabase/src/database.types';
|
||||||
|
import Isikukood from 'isikukood';
|
||||||
import { Clock, TrendingUp, User } from 'lucide-react';
|
import { Clock, TrendingUp, User } from 'lucide-react';
|
||||||
|
|
||||||
import { bmiFromMetric } from '~/lib/utils';
|
import {
|
||||||
|
bmiFromMetric,
|
||||||
|
getBmiBackgroundColor,
|
||||||
|
getBmiStatus,
|
||||||
|
} from '~/lib/utils';
|
||||||
|
|
||||||
import { TeamAccountStatisticsProps } from '../../_components/team-account-statistics';
|
import { TeamAccountStatisticsProps } from '../../_components/team-account-statistics';
|
||||||
|
|
||||||
@@ -14,66 +20,89 @@ interface AccountHealthDetailsField {
|
|||||||
color?: string;
|
color?: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
}>;
|
}>;
|
||||||
normStatus: NormStatus;
|
iconBg: string;
|
||||||
}
|
|
||||||
|
|
||||||
export enum NormStatus {
|
|
||||||
CRITICAL = 'CRITICAL',
|
|
||||||
WARNING = 'WARNING',
|
|
||||||
NORMAL = 'NORMAL',
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getAccountHealthDetailsFields = (
|
export const getAccountHealthDetailsFields = (
|
||||||
memberParams: TeamAccountStatisticsProps['memberParams'],
|
memberParams: TeamAccountStatisticsProps['memberParams'],
|
||||||
|
bmiThresholds: Omit<
|
||||||
|
Database['medreport']['Tables']['bmi_thresholds']['Row'],
|
||||||
|
'id'
|
||||||
|
>[],
|
||||||
|
members: Database['medreport']['Functions']['get_account_members']['Returns'],
|
||||||
): AccountHealthDetailsField[] => {
|
): AccountHealthDetailsField[] => {
|
||||||
const averageBMI = (
|
const avarageWeight =
|
||||||
memberParams.reduce((sum, { height, weight }) => {
|
memberParams.reduce((sum, r) => sum + r.weight!, 0) / memberParams.length;
|
||||||
return bmiFromMetric(weight ?? 0, height ?? 0) + sum;
|
const avarageHeight =
|
||||||
}, 0) / memberParams.length
|
memberParams.reduce((sum, r) => sum + r.height!, 0) / memberParams.length;
|
||||||
).toFixed(0);
|
const avarageAge =
|
||||||
|
members.reduce((sum, r) => {
|
||||||
|
const person = new Isikukood(r.personal_code);
|
||||||
|
return sum + person.getAge();
|
||||||
|
}, 0) / members.length;
|
||||||
|
const numberOfMaleMembers = members.filter((r) => {
|
||||||
|
const person = new Isikukood(r.personal_code);
|
||||||
|
return person.getGender() === 'male';
|
||||||
|
}).length;
|
||||||
|
const numberOfFemaleMembers = members.filter((r) => {
|
||||||
|
const person = new Isikukood(r.personal_code);
|
||||||
|
return person.getGender() === 'female';
|
||||||
|
}).length;
|
||||||
|
const averageBMI = bmiFromMetric(avarageWeight, avarageHeight);
|
||||||
|
const bmiStatus = getBmiStatus(bmiThresholds, {
|
||||||
|
age: avarageAge,
|
||||||
|
height: avarageHeight,
|
||||||
|
weight: avarageWeight,
|
||||||
|
});
|
||||||
|
const malePercentage = members.length
|
||||||
|
? (numberOfMaleMembers / members.length) * 100
|
||||||
|
: 0;
|
||||||
|
const femalePercentage = members.length
|
||||||
|
? (numberOfFemaleMembers / members.length) * 100
|
||||||
|
: 0;
|
||||||
|
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
title: 'teams:healthDetails.women',
|
title: 'teams:healthDetails.women',
|
||||||
value: `50% (${memberParams.length})`,
|
value: `${femalePercentage}% (${numberOfFemaleMembers})`,
|
||||||
Icon: User,
|
Icon: User,
|
||||||
normStatus: NormStatus.NORMAL,
|
iconBg: 'bg-success',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'teams:healthDetails.men',
|
title: 'teams:healthDetails.men',
|
||||||
value: `50% (${memberParams.length})`,
|
value: `${malePercentage}% (${numberOfMaleMembers})`,
|
||||||
Icon: User,
|
Icon: User,
|
||||||
normStatus: NormStatus.NORMAL,
|
iconBg: 'bg-success',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'teams:healthDetails.avgAge',
|
title: 'teams:healthDetails.avgAge',
|
||||||
value: '56',
|
value: avarageAge.toFixed(0),
|
||||||
Icon: Clock,
|
Icon: Clock,
|
||||||
normStatus: NormStatus.NORMAL,
|
iconBg: 'bg-success',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'teams:healthDetails.bmi',
|
title: 'teams:healthDetails.bmi',
|
||||||
value: averageBMI,
|
value: averageBMI,
|
||||||
Icon: TrendingUp,
|
Icon: TrendingUp,
|
||||||
normStatus: NormStatus.WARNING,
|
iconBg: getBmiBackgroundColor(bmiStatus),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'teams:healthDetails.cholesterol',
|
title: 'teams:healthDetails.cholesterol',
|
||||||
value: '6.1',
|
value: '-',
|
||||||
Icon: TrendingUp,
|
Icon: TrendingUp,
|
||||||
normStatus: NormStatus.WARNING,
|
iconBg: 'bg-warning',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'teams:healthDetails.vitaminD',
|
title: 'teams:healthDetails.vitaminD',
|
||||||
value: '76',
|
value: '-',
|
||||||
Icon: TrendingUp,
|
Icon: TrendingUp,
|
||||||
normStatus: NormStatus.NORMAL,
|
iconBg: 'bg-warning',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'teams:healthDetails.smokers',
|
title: 'teams:healthDetails.smokers',
|
||||||
value: '22%',
|
value: '-',
|
||||||
Icon: TrendingUp,
|
Icon: TrendingUp,
|
||||||
normStatus: NormStatus.CRITICAL,
|
iconBg: 'bg-warning',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import { use } from 'react';
|
import { use } from 'react';
|
||||||
|
|
||||||
|
import { createAccountsApi } from '@/packages/features/accounts/src/server/api';
|
||||||
import { CompanyGuard } from '@/packages/features/team-accounts/src/components';
|
import { CompanyGuard } from '@/packages/features/team-accounts/src/components';
|
||||||
import { createTeamAccountsApi } from '@/packages/features/team-accounts/src/server/api';
|
import { createTeamAccountsApi } from '@/packages/features/team-accounts/src/server/api';
|
||||||
import { getSupabaseServerClient } from '@/packages/supabase/src/clients/server-client';
|
import { getSupabaseServerClient } from '@/packages/supabase/src/clients/server-client';
|
||||||
@@ -11,6 +12,10 @@ import { Trans } from '@kit/ui/trans';
|
|||||||
|
|
||||||
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
|
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
|
||||||
import { withI18n } from '~/lib/i18n/with-i18n';
|
import { withI18n } from '~/lib/i18n/with-i18n';
|
||||||
|
import {
|
||||||
|
PAGE_VIEW_ACTION,
|
||||||
|
createPageViewLog,
|
||||||
|
} from '~/lib/services/audit/pageView.service';
|
||||||
|
|
||||||
import { Dashboard } from './_components/dashboard';
|
import { Dashboard } from './_components/dashboard';
|
||||||
import { TeamAccountLayoutPageHeader } from './_components/team-account-layout-page-header';
|
import { TeamAccountLayoutPageHeader } from './_components/team-account-layout-page-header';
|
||||||
@@ -31,25 +36,32 @@ export const generateMetadata = async () => {
|
|||||||
function TeamAccountHomePage({ params }: TeamAccountHomePageProps) {
|
function TeamAccountHomePage({ params }: TeamAccountHomePageProps) {
|
||||||
const account = use(params).account;
|
const account = use(params).account;
|
||||||
const client = getSupabaseServerClient();
|
const client = getSupabaseServerClient();
|
||||||
const api = createTeamAccountsApi(client);
|
const teamAccountsApi = createTeamAccountsApi(client);
|
||||||
const teamAccount = use(api.getTeamAccount(account));
|
const accountsApi = createAccountsApi(client);
|
||||||
const { memberParams } = use(api.getMembers(account));
|
const teamAccount = use(teamAccountsApi.getTeamAccount(account));
|
||||||
|
const { memberParams, members } = use(teamAccountsApi.getMembers(account));
|
||||||
|
const bmiThresholds = use(accountsApi.fetchBmiThresholds());
|
||||||
|
const companyParams = use(
|
||||||
|
teamAccountsApi.getTeamAccountParams(teamAccount.id),
|
||||||
|
);
|
||||||
|
|
||||||
|
use(
|
||||||
|
createPageViewLog({
|
||||||
|
accountId: teamAccount.id,
|
||||||
|
action: PAGE_VIEW_ACTION.VIEW_TEAM_ACCOUNT_DASHBOARD,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<PageBody>
|
||||||
<TeamAccountLayoutPageHeader
|
<Dashboard
|
||||||
title={
|
teamAccount={teamAccount}
|
||||||
<Trans
|
memberParams={memberParams}
|
||||||
i18nKey={'teams:home.headerTitle'}
|
bmiThresholds={bmiThresholds}
|
||||||
values={{ companyName: account }}
|
members={members}
|
||||||
/>
|
companyParams={companyParams}
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
|
</PageBody>
|
||||||
<PageBody>
|
|
||||||
<Dashboard teamAccount={teamAccount} memberParams={memberParams} />
|
|
||||||
</PageBody>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ export enum PAGE_VIEW_ACTION {
|
|||||||
VIEW_ANALYSIS_RESULTS = 'VIEW_ANALYSIS_RESULTS',
|
VIEW_ANALYSIS_RESULTS = 'VIEW_ANALYSIS_RESULTS',
|
||||||
REGISTRATION_SUCCESS = 'REGISTRATION_SUCCESS',
|
REGISTRATION_SUCCESS = 'REGISTRATION_SUCCESS',
|
||||||
VIEW_ORDER_ANALYSIS = 'VIEW_ORDER_ANALYSIS',
|
VIEW_ORDER_ANALYSIS = 'VIEW_ORDER_ANALYSIS',
|
||||||
|
VIEW_TEAM_ACCOUNT_DASHBOARD = 'VIEW_TEAM_ACCOUNT_DASHBOARD',
|
||||||
}
|
}
|
||||||
|
|
||||||
export const createPageViewLog = async ({
|
export const createPageViewLog = async ({
|
||||||
|
|||||||
@@ -70,15 +70,13 @@ export function getBmiStatus(
|
|||||||
export function getBmiBackgroundColor(bmiStatus: BmiCategory | null): string {
|
export function getBmiBackgroundColor(bmiStatus: BmiCategory | null): string {
|
||||||
switch (bmiStatus) {
|
switch (bmiStatus) {
|
||||||
case BmiCategory.UNDER_WEIGHT:
|
case BmiCategory.UNDER_WEIGHT:
|
||||||
|
case BmiCategory.OVER_WEIGHT:
|
||||||
return 'bg-warning';
|
return 'bg-warning';
|
||||||
case BmiCategory.NORMAL:
|
case BmiCategory.NORMAL:
|
||||||
return 'bg-success';
|
return 'bg-success';
|
||||||
case BmiCategory.OVER_WEIGHT:
|
|
||||||
return 'bg-warning';
|
|
||||||
case BmiCategory.VERY_OVERWEIGHT:
|
case BmiCategory.VERY_OVERWEIGHT:
|
||||||
return 'bg-destructive';
|
|
||||||
case BmiCategory.OBESE:
|
case BmiCategory.OBESE:
|
||||||
return 'bg-error';
|
return 'bg-destructive';
|
||||||
default:
|
default:
|
||||||
return 'bg-success';
|
return 'bg-success';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -291,6 +291,7 @@ export class TeamAccountsApi {
|
|||||||
.single();
|
.single();
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
|
console.warn('Error fetching company params', error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1726,6 +1726,7 @@ export type Database = {
|
|||||||
primary_owner_user_id: string
|
primary_owner_user_id: string
|
||||||
name: string
|
name: string
|
||||||
email: string
|
email: string
|
||||||
|
personal_code: string
|
||||||
picture_url: string
|
picture_url: string
|
||||||
created_at: string
|
created_at: string
|
||||||
updated_at: string
|
updated_at: string
|
||||||
|
|||||||
@@ -122,7 +122,8 @@
|
|||||||
"weight": "Kaal",
|
"weight": "Kaal",
|
||||||
"height": "Pikkus",
|
"height": "Pikkus",
|
||||||
"occurance": "Toetuse sagedus",
|
"occurance": "Toetuse sagedus",
|
||||||
"amount": "Summa"
|
"amount": "Summa",
|
||||||
|
"date": "Vali kuupäev"
|
||||||
},
|
},
|
||||||
"wallet": {
|
"wallet": {
|
||||||
"balance": "Sinu MedReporti konto seis",
|
"balance": "Sinu MedReporti konto seis",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"home": {
|
"home": {
|
||||||
"pageTitle": "Ülevaade",
|
"pageTitle": "Ülevaade",
|
||||||
"headerTitle": "{{companyName}} tervise ülevaade",
|
"headerTitle": "{{companyName}} Tervisekassa kokkuvõte",
|
||||||
"healthDetails": "Ettevõtte terviseandmed",
|
"healthDetails": "Ettevõtte terviseandmed",
|
||||||
"membersSettingsButtonTitle": "Halda töötajaid",
|
"membersSettingsButtonTitle": "Halda töötajaid",
|
||||||
"membersSettingsButtonDescription": "Lisa, muuda või eemalda töötajaid.",
|
"membersSettingsButtonDescription": "Lisa, muuda või eemalda töötajaid.",
|
||||||
@@ -31,13 +31,14 @@
|
|||||||
"volume": "Eelarve maht {{volume}}"
|
"volume": "Eelarve maht {{volume}}"
|
||||||
},
|
},
|
||||||
"data": {
|
"data": {
|
||||||
"reservations": "{{value}} broneeringut",
|
"reservations": "{{value}} teenust",
|
||||||
"analysis": "Analüüsid",
|
"analysis": "Analüüsid",
|
||||||
"doctorsAndSpecialists": "Eriarstid ja spetsialistid",
|
"doctorsAndSpecialists": "Eriarstid ja spetsialistid",
|
||||||
"researches": "Uuringud",
|
"researches": "Uuringud",
|
||||||
"healthResearchPlans": "Terviseuuringute paketid",
|
"healthResearchPlans": "Terviseuuringute paketid",
|
||||||
"serviceUsage": "{{value}} teenuse kasutust",
|
"serviceUsage": "{{value}} teenuse kasutust",
|
||||||
"serviceSum": "Teenuste summa"
|
"serviceSum": "Teenuste summa",
|
||||||
|
"eclinic": "Digikliinik"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"healthDetails": {
|
"healthDetails": {
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
DROP FUNCTION IF EXISTS medreport.get_account_members(text);
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION medreport.get_account_members(account_slug text)
|
||||||
|
RETURNS TABLE(
|
||||||
|
id uuid,
|
||||||
|
user_id uuid,
|
||||||
|
account_id uuid,
|
||||||
|
role character varying,
|
||||||
|
role_hierarchy_level integer,
|
||||||
|
primary_owner_user_id uuid,
|
||||||
|
name character varying,
|
||||||
|
email character varying,
|
||||||
|
personal_code text,
|
||||||
|
picture_url character varying,
|
||||||
|
created_at timestamp with time zone,
|
||||||
|
updated_at timestamp with time zone
|
||||||
|
)
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
SET search_path TO ''
|
||||||
|
AS $function$begin
|
||||||
|
return QUERY
|
||||||
|
select
|
||||||
|
acc.id,
|
||||||
|
am.user_id,
|
||||||
|
am.account_id,
|
||||||
|
am.account_role,
|
||||||
|
r.hierarchy_level,
|
||||||
|
a.primary_owner_user_id,
|
||||||
|
acc.name,
|
||||||
|
acc.email,
|
||||||
|
acc.personal_code,
|
||||||
|
acc.picture_url,
|
||||||
|
am.created_at,
|
||||||
|
am.updated_at
|
||||||
|
from
|
||||||
|
medreport.accounts_memberships am
|
||||||
|
join medreport.accounts a on a.id = am.account_id
|
||||||
|
join medreport.accounts acc on acc.id = am.user_id
|
||||||
|
join medreport.roles r on r.name = am.account_role
|
||||||
|
where
|
||||||
|
a.slug = account_slug;
|
||||||
|
|
||||||
|
end;$function$
|
||||||
|
;
|
||||||
|
|
||||||
|
grant
|
||||||
|
execute on function medreport.get_account_members (text) to authenticated,
|
||||||
|
service_role;
|
||||||
Reference in New Issue
Block a user