diff --git a/backend/src/modules/organization/organization.controller.ts b/backend/src/modules/organization/organization.controller.ts
index 7e56c60..7f0fc68 100644
--- a/backend/src/modules/organization/organization.controller.ts
+++ b/backend/src/modules/organization/organization.controller.ts
@@ -64,6 +64,14 @@ export class OrganizationController {
return this.organizationService.searchCounterpartOrganizations(req.user.id, organizationId, q);
}
+ @Get('connections/pending-count')
+ @UseGuards(JwtAuthGuard)
+ @ApiOperation({ summary: 'Count incoming pending connection requests for sidebar badge' })
+ countPendingConnections(@Req() req: { user: { id: string; organizationId?: string } }) {
+ const organizationId = this.organizationService.getOrganizationIdFromUser(req.user);
+ return this.organizationService.countIncomingPendingConnections(req.user.id, organizationId);
+ }
+
@Get('connections')
@UseGuards(JwtAuthGuard)
@ApiOperation({ summary: 'List counterpart connections for current organization' })
diff --git a/backend/src/modules/organization/organization.service.ts b/backend/src/modules/organization/organization.service.ts
index 5198d77..6945d09 100644
--- a/backend/src/modules/organization/organization.service.ts
+++ b/backend/src/modules/organization/organization.service.ts
@@ -160,6 +160,29 @@ export class OrganizationService {
};
}
+ /** Lightweight count for sidebar badge — incoming PENDING requests only. */
+ async countIncomingPendingConnections(userId: string, organizationId: string) {
+ const actor = await this.getActorMembership(userId, organizationId);
+ if (!actor || !this.canEditOrganizations(actor)) {
+ throw new ForbiddenException('You do not have permission to manage organizations');
+ }
+
+ const links = await this.prisma.organizationLink.findMany({
+ where: {
+ status: LinkStatus.PENDING,
+ OR: [{ organizationAId: organizationId }, { organizationBId: organizationId }],
+ },
+ select: { sharedDataTypes: true },
+ });
+
+ const count = links.filter((link) => {
+ const requesterOrgId = this.getRequesterOrganizationId(link.sharedDataTypes);
+ return requesterOrgId !== null && requesterOrgId !== organizationId;
+ }).length;
+
+ return { success: true, data: { count } };
+ }
+
async listInvitationHistory(userId: string, organizationId: string) {
const actor = await this.getActorMembership(userId, organizationId);
if (!actor || !this.canEditOrganizations(actor)) {
diff --git a/frontend/src/app/[locale]/(dashboard)/organizations/page.tsx b/frontend/src/app/[locale]/(dashboard)/organizations/page.tsx
index 87b9f47..c0ffcfc 100644
--- a/frontend/src/app/[locale]/(dashboard)/organizations/page.tsx
+++ b/frontend/src/app/[locale]/(dashboard)/organizations/page.tsx
@@ -5,6 +5,7 @@ import { useTranslations } from 'next-intl';
import { useToast } from '@/lib/hooks/useToast';
import { Check, Trash2, UserPlus, X } from 'lucide-react';
import { useAuth } from '@/lib/hooks/useAuth';
+import { notifyPendingConnectionsChanged } from '@/lib/hooks/usePendingConnectionsCount';
import { useOrganizationInviteLinkCopy } from '@/lib/hooks/useOrganizationInviteLinkCopy';
import {
organizationApi,
@@ -254,6 +255,7 @@ export default function OrganizationsPage() {
toast.showSuccess(
action === 'ACCEPT' ? t('successAccepted') : t('successDeclined'),
);
+ notifyPendingConnectionsChanged();
await loadList();
} catch (e) {
toast.showError(formatApiMessage(e));
diff --git a/frontend/src/components/ui/shared/Sidebar.tsx b/frontend/src/components/ui/shared/Sidebar.tsx
index ce64b41..6e55a0c 100644
--- a/frontend/src/components/ui/shared/Sidebar.tsx
+++ b/frontend/src/components/ui/shared/Sidebar.tsx
@@ -13,6 +13,7 @@ import {
CreditCard,
} from 'lucide-react';
import { useAuth } from '@/lib/hooks/useAuth';
+import { usePendingConnectionsCount } from '@/lib/hooks/usePendingConnectionsCount';
import { canAccessAppointmentsSection, canViewTab } from '@/components/shared/permissions';
import {
counterpartOrganizationType,
@@ -24,6 +25,8 @@ function Sidebar() {
const tCommon = useTranslations('common');
const pathname = usePathname();
const { currentOrganization } = useAuth();
+ const pendingConnectionsCount = usePendingConnectionsCount();
+
const menu = useMemo(
() => [
@@ -66,6 +69,8 @@ function Sidebar() {
{visibleMenu.map((item) => {
const Icon = item.icon;
const isActive = pathname === item.path;
+ const showPendingBadge =
+ item.path === '/organizations' && pendingConnectionsCount > 0;
return (
- {item.name}
+ {item.name}
+ {showPendingBadge && (
+
+ {pendingConnectionsCount}
+
+ )}
);
})}
diff --git a/frontend/src/lib/api/organization.ts b/frontend/src/lib/api/organization.ts
index 7c940ce..0e54cd8 100644
--- a/frontend/src/lib/api/organization.ts
+++ b/frontend/src/lib/api/organization.ts
@@ -46,6 +46,11 @@ export const organizationApi = {
return response.data;
},
+ pendingIncomingCount: async (): Promise<{ success: boolean; data: { count: number } }> => {
+ const response = await apiClient.get('/organizations/connections/pending-count');
+ return response.data;
+ },
+
listInvitations: async (): Promise<{
success: boolean;
data: { items: OrganizationInvitationHistoryItemDto[] };
diff --git a/frontend/src/lib/hooks/usePendingConnectionsCount.ts b/frontend/src/lib/hooks/usePendingConnectionsCount.ts
new file mode 100644
index 0000000..4be203b
--- /dev/null
+++ b/frontend/src/lib/hooks/usePendingConnectionsCount.ts
@@ -0,0 +1,48 @@
+'use client';
+
+import { useCallback, useEffect, useState } from 'react';
+import { usePathname } from 'next/navigation';
+import { canViewTab } from '@/components/shared/permissions';
+import { organizationApi } from '@/lib/api/organization';
+import { useAuth } from '@/lib/hooks/useAuth';
+
+/** Tell the sidebar badge to refetch after accept/decline on the organizations page. */
+export function notifyPendingConnectionsChanged() {
+ window.dispatchEvent(new Event('pending-connections-changed'));
+}
+
+/**
+ * Sidebar-only: fetches GET /organizations/connections/pending-count.
+ * Independent from the organizations tab list API.
+ */
+export function usePendingConnectionsCount(): number {
+ const pathname = usePathname();
+ const { currentOrganization } = useAuth();
+ const [count, setCount] = useState(0);
+
+ const fetchCount = useCallback(async () => {
+ if (!currentOrganization?.id || !canViewTab(currentOrganization, 'TAB_ORGANIZATIONS_READ')) {
+ setCount(0);
+ return;
+ }
+
+ try {
+ const res = await organizationApi.pendingIncomingCount();
+ setCount(res.data.count);
+ } catch {
+ setCount(0);
+ }
+ }, [currentOrganization]);
+
+ useEffect(() => {
+ void fetchCount();
+ }, [fetchCount, pathname]);
+
+ useEffect(() => {
+ const onChanged = () => void fetchCount();
+ window.addEventListener('pending-connections-changed', onChanged);
+ return () => window.removeEventListener('pending-connections-changed', onChanged);
+ }, [fetchCount]);
+
+ return count;
+}