fix(sidebar): show org settings link for platform admins without memberships

Platform admins query GET /organizations (not memberships) so currentOrgId
was always null — hiding the Settings nav link. Now falls back to the first
org from useOrganizations() for admins, gated with enabled:isPlatformAdmin
to avoid 403 for non-admin roles.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Vadym Samoilenko 2026-05-01 17:45:49 +01:00
parent e60e7c96e7
commit 27286e23db
2 changed files with 54 additions and 25 deletions

View file

@ -1,6 +1,6 @@
import { Link, useLocation, useParams } from 'react-router-dom';
import { useAuthStore } from '../../lib/auth';
import { useMyMemberships } from '../../hooks/useClients';
import { useMyMemberships, useOrganizations } from '../../hooks/useClients';
import { useJobs, useBriefs, useMyQCQueueCount } from '../../hooks/useJob';
interface SidebarItem {
@ -24,6 +24,7 @@ export function Sidebar({ onMobileClose }: SidebarProps) {
const isQCRole = ['linguist', 'reviewer', 'production', 'admin'].includes(user?.role || '');
const isPMOrAdmin = ['project_manager', 'admin'].includes(user?.role || '');
const isAdminOrProduction = ['production', 'admin'].includes(user?.role || '');
const isPlatformAdmin = user?.role === 'admin';
const myQCCount = useMyQCQueueCount(isQCRole);
const { data: finalData } = useJobs(
@ -36,18 +37,20 @@ export function Sidebar({ onMobileClose }: SidebarProps) {
);
const { data: allBriefs = [] } = useBriefs();
const { data: allOrgs = [] } = useOrganizations({ enabled: isPlatformAdmin });
const qcBadge = isQCRole ? myQCCount : 0;
const finalBadge = isPMOrAdmin ? (finalData?.total || 0) : 0;
const failuresBadge = isAdminOrProduction ? (failuresData?.total || 0) : 0;
const briefsBadge = allBriefs.filter(b => b.status === 'submitted').length;
// Determine current org ID from route params or first membership.
// Determine current org ID from route params, first membership, or first org (admin fallback).
// The route param :orgSlug actually carries the organization _id (hex string),
// not the human-readable slug — the backend queries memberships by organization_id.
const currentOrgId =
params.orgSlug ||
(memberships.length === 1 ? memberships[0].organization_id : null);
(memberships.length > 0 ? memberships[0].organization_id : null) ||
(isPlatformAdmin && allOrgs.length > 0 ? allOrgs[0].id : null);
const sidebarItems: SidebarItem[] = [
{
@ -152,29 +155,54 @@ export function Sidebar({ onMobileClose }: SidebarProps) {
</div>
</div>
{/* Org Switcher — shown when user has memberships */}
{memberships.length > 0 && (
{/* Org Switcher — shown when user has memberships or is platform admin with orgs */}
{(memberships.length > 0 || (isPlatformAdmin && allOrgs.length > 0)) && (
<div className="px-4 py-3 border-b border-gray-100 bg-gray-50">
{memberships.length === 1 ? (
<div className="text-xs text-gray-500">
<span className="font-medium text-gray-700">{memberships[0].organization_name}</span>
<span className="ml-1 capitalize text-gray-400">· {memberships[0].role_in_org}</span>
</div>
{memberships.length > 0 ? (
memberships.length === 1 ? (
<div className="text-xs text-gray-500">
<span className="font-medium text-gray-700">{memberships[0].organization_name}</span>
<span className="ml-1 capitalize text-gray-400">· {memberships[0].role_in_org}</span>
</div>
) : (
<select
value={currentOrgId || ''}
onChange={(e) => {
const orgId = e.target.value;
window.location.href = `/video-accessibility/org/${orgId}/settings/members`;
}}
className="w-full text-xs border-0 bg-transparent text-gray-700 font-medium focus:outline-none cursor-pointer"
>
{memberships.map(m => (
<option key={m.organization_id} value={m.organization_id}>
{m.organization_name}
</option>
))}
</select>
)
) : (
<select
value={currentOrgId || ''}
onChange={(e) => {
const orgId = e.target.value;
window.location.href = `/video-accessibility/org/${orgId}/settings/members`;
}}
className="w-full text-xs border-0 bg-transparent text-gray-700 font-medium focus:outline-none cursor-pointer"
>
{memberships.map(m => (
<option key={m.organization_id} value={m.organization_id}>
{m.organization_name}
</option>
))}
</select>
/* Platform admin with no personal memberships — show org switcher from full org list */
allOrgs.length === 1 ? (
<div className="text-xs text-gray-500">
<span className="font-medium text-gray-700">{allOrgs[0].name}</span>
<span className="ml-1 text-gray-400">· admin</span>
</div>
) : (
<select
value={currentOrgId || ''}
onChange={(e) => {
const orgId = e.target.value;
window.location.href = `/video-accessibility/org/${orgId}/settings/members`;
}}
className="w-full text-xs border-0 bg-transparent text-gray-700 font-medium focus:outline-none cursor-pointer"
>
{allOrgs.map(org => (
<option key={org.id} value={org.id}>
{org.name}
</option>
))}
</select>
)
)}
</div>
)}

View file

@ -223,10 +223,11 @@ export function useArchiveProject(clientId: string) {
// ── Organizations (SaaS) ──────────────────────────────────────────────────────
export function useOrganizations() {
export function useOrganizations(options?: { enabled?: boolean }) {
return useQuery({
queryKey: ['organizations'],
queryFn: () => apiClient.listOrganizations(),
enabled: options?.enabled !== false,
});
}