133 lines
4.9 KiB
TypeScript
133 lines
4.9 KiB
TypeScript
'use client';
|
|
|
|
import { memo, useMemo } from 'react';
|
|
import { useTranslations } from 'next-intl';
|
|
import { Link, usePathname } from '@/i18n/navigation';
|
|
import {
|
|
LayoutDashboard,
|
|
Users,
|
|
Calendar,
|
|
UserCog,
|
|
FlaskConical,
|
|
FileText,
|
|
CreditCard,
|
|
Package,
|
|
ListTodo,
|
|
} from 'lucide-react';
|
|
import type { OrgTypeName } from '@/components/shared/permissions';
|
|
import { useAuth } from '@/lib/hooks/useAuth';
|
|
import { usePendingConnectionsCount } from '@/lib/hooks/usePendingConnectionsCount';
|
|
import {
|
|
canViewAppointmentsTab,
|
|
canViewCases,
|
|
canViewTasks,
|
|
canViewTab,
|
|
} from '@/components/shared/permissions';
|
|
import {
|
|
counterpartOrganizationType,
|
|
organizationTypeIcon,
|
|
} from '@/components/shared/organizationTypeIcon';
|
|
|
|
type MenuItem = {
|
|
name: string;
|
|
path: string;
|
|
icon: typeof LayoutDashboard;
|
|
read: string;
|
|
orgTypes: OrgTypeName[];
|
|
};
|
|
|
|
function Sidebar() {
|
|
const t = useTranslations('nav');
|
|
const tCommon = useTranslations('common');
|
|
const pathname = usePathname();
|
|
const { currentOrganization } = useAuth();
|
|
const pendingConnectionsCount = usePendingConnectionsCount();
|
|
const orgType = currentOrganization?.type;
|
|
|
|
const menu = useMemo((): MenuItem[] => {
|
|
const items: MenuItem[] = [
|
|
{ name: t('dashboard'), path: '/today', icon: LayoutDashboard, read: 'TAB_TODAY_READ', orgTypes: ['CLINIC', 'LAB'] },
|
|
{ name: t('staff'), path: '/staff', icon: UserCog, read: 'TAB_STAFF_READ', orgTypes: ['CLINIC', 'LAB'] },
|
|
{
|
|
name: orgType === 'LAB' ? t('clinics') : t('labs'),
|
|
path: '/organizations',
|
|
icon: organizationTypeIcon(counterpartOrganizationType(orgType)),
|
|
read: 'TAB_ORGANIZATIONS_READ',
|
|
orgTypes: ['CLINIC', 'LAB'],
|
|
},
|
|
{ name: t('patients'), path: '/patients', icon: Users, read: 'TAB_PATIENTS_READ', orgTypes: ['CLINIC'] },
|
|
{ name: t('cases'), path: '/cases', icon: Package, read: 'TAB_CASES_READ', orgTypes: ['LAB'] },
|
|
{ name: t('tasks'), path: '/tasks', icon: ListTodo, read: 'TAB_TASKS_READ', orgTypes: ['LAB'] },
|
|
{ name: t('appointment'), path: '/appointments', icon: Calendar, read: 'TAB_APPOINTMENTS_READ', orgTypes: ['CLINIC'] },
|
|
{ name: t('treatment'), path: '/treatment', icon: FlaskConical, read: 'TAB_TREATMENT_READ', orgTypes: ['CLINIC'] },
|
|
{ name: t('billing'), path: '/billing', icon: CreditCard, read: 'TAB_BILLING_READ', orgTypes: ['CLINIC', 'LAB'] },
|
|
{ name: t('reports'), path: '/reports', icon: FileText, read: 'TAB_REPORTS_READ', orgTypes: ['CLINIC', 'LAB'] },
|
|
];
|
|
return items;
|
|
}, [orgType, t]);
|
|
|
|
const visibleMenu = useMemo(
|
|
() =>
|
|
menu.filter((item) => {
|
|
if (!orgType || !item.orgTypes.includes(orgType)) {
|
|
return false;
|
|
}
|
|
if (item.path === '/appointments') {
|
|
return canViewAppointmentsTab(currentOrganization);
|
|
}
|
|
if (item.path === '/cases') {
|
|
return canViewCases(currentOrganization);
|
|
}
|
|
if (item.path === '/tasks') {
|
|
return canViewTasks(currentOrganization);
|
|
}
|
|
return canViewTab(currentOrganization, item.read);
|
|
}),
|
|
[currentOrganization, menu, orgType],
|
|
);
|
|
|
|
return (
|
|
<aside className="w-56 min-w-56 shrink-0 bg-background-secondary/90 border-r border-border text-text-primary flex flex-col">
|
|
<div className="h-[71px] px-4 flex items-center">
|
|
<h1 className="text-lg font-medium tracking-tight">{tCommon('appName')}</h1>
|
|
</div>
|
|
<div className="mx-4 border-b border-border/70" />
|
|
|
|
<nav className="flex flex-col gap-2 p-4">
|
|
{visibleMenu.map((item) => {
|
|
const Icon = item.icon;
|
|
const isActive = pathname === item.path;
|
|
const showPendingBadge =
|
|
item.path === '/organizations' && pendingConnectionsCount > 0;
|
|
|
|
return (
|
|
<Link
|
|
key={item.path}
|
|
href={item.path}
|
|
prefetch
|
|
className={`flex items-center gap-3 px-3 py-2.5 rounded-[var(--radius-sm)] border transition-colors ${
|
|
isActive
|
|
? 'bg-primary-soft border-primary/60 text-text-primary'
|
|
: 'border-border/50 text-text-secondary hover:bg-background-card/70 hover:text-text-primary hover:border-border'
|
|
}`}
|
|
>
|
|
<Icon className="w-[18px] h-[18px] icon-flat" />
|
|
<span className="text-sm flex-1">{item.name}</span>
|
|
{showPendingBadge && (
|
|
<span
|
|
className="min-w-[1.25rem] rounded-full bg-badge-warning-bg px-1.5 py-0.5 text-center text-xs font-medium tabular-nums text-badge-warning-fg border border-badge-warning-border"
|
|
aria-label={`${pendingConnectionsCount} pending connection requests`}
|
|
>
|
|
{pendingConnectionsCount}
|
|
</span>
|
|
)}
|
|
</Link>
|
|
);
|
|
})}
|
|
</nav>
|
|
</aside>
|
|
);
|
|
}
|
|
|
|
export default memo(Sidebar);
|