39443-vm/frontend/src/components/MyOrgTenantSummary.tsx
2026-04-04 15:53:16 +00:00

204 lines
6.9 KiB
TypeScript

import React, { useEffect, useMemo, useState } from 'react'
import CardBox from './CardBox'
import ConnectedEntityCard from './ConnectedEntityCard'
import TenantStatusChip from './TenantStatusChip'
import { useAppSelector } from '../stores/hooks'
import {
emptyOrganizationTenantSummary,
getOrganizationViewHref,
getTenantViewHref,
loadLinkedTenantSummary,
} from '../helpers/organizationTenants'
import { hasPermission } from '../helpers/userPermissions'
type MyOrgTenantSummaryProps = {
className?: string
}
const MyOrgTenantSummary = ({ className = '' }: MyOrgTenantSummaryProps) => {
const { currentUser } = useAppSelector((state) => state.auth)
const [linkedTenantSummary, setLinkedTenantSummary] = useState(emptyOrganizationTenantSummary)
const [isLoadingTenants, setIsLoadingTenants] = useState(false)
const organizationId = useMemo(
() =>
currentUser?.organizations?.id ||
currentUser?.organization?.id ||
currentUser?.organizationsId ||
currentUser?.organizationId ||
'',
[
currentUser?.organization?.id,
currentUser?.organizationId,
currentUser?.organizations?.id,
currentUser?.organizationsId,
],
)
const organizationName =
currentUser?.organizations?.name ||
currentUser?.organization?.name ||
currentUser?.organizationName ||
'No organization assigned yet'
const canViewOrganizations = hasPermission(currentUser, 'READ_ORGANIZATIONS')
const canViewTenants = hasPermission(currentUser, 'READ_TENANTS')
useEffect(() => {
let isMounted = true
if (!organizationId || !canViewTenants) {
setLinkedTenantSummary(emptyOrganizationTenantSummary)
setIsLoadingTenants(false)
return () => {
isMounted = false
}
}
setIsLoadingTenants(true)
loadLinkedTenantSummary(organizationId)
.then((summary) => {
if (!isMounted) {
return
}
setLinkedTenantSummary(summary)
})
.catch((error) => {
console.error('Failed to load current user org/tenant context:', error)
if (!isMounted) {
return
}
setLinkedTenantSummary(emptyOrganizationTenantSummary)
})
.finally(() => {
if (isMounted) {
setIsLoadingTenants(false)
}
})
return () => {
isMounted = false
}
}, [canViewTenants, organizationId])
if (!currentUser) {
return null
}
const appRoleName = currentUser?.app_role?.name || 'No role surfaced yet'
const linkedTenants = Array.isArray(linkedTenantSummary.rows) ? linkedTenantSummary.rows : []
const tenantSummaryValue = !organizationId
? 'No workspace linked'
: !canViewTenants
? 'Restricted'
: isLoadingTenants
? 'Loading…'
: String(linkedTenantSummary.count || 0)
const tenantEmptyStateMessage = !organizationId
? 'No organization is attached to this account yet.'
: !canViewTenants
? 'Your current role does not include tenant-read access for this organization context.'
: 'No tenant link surfaced yet for your organization.'
return (
<CardBox className={className}>
<div className="space-y-4">
<div>
<div className="inline-flex rounded-full border border-blue-200 bg-blue-50 px-2.5 py-1 text-[11px] font-semibold uppercase tracking-[0.18em] text-blue-900 dark:border-blue-900 dark:bg-blue-950/40 dark:text-blue-100">
My account context
</div>
<h3 className="mt-3 text-lg font-semibold text-gray-900 dark:text-gray-100">
Your organization and tenant access
</h3>
<p className="mt-1 text-sm text-gray-600 dark:text-gray-300">
This makes it clear which organization your signed-in account belongs to and which tenant links sit behind it.
</p>
</div>
<ConnectedEntityCard
entityLabel="My organization"
title={organizationName}
titleFallback="No organization assigned yet"
details={[
{ label: 'Account role', value: appRoleName },
{ label: 'Email', value: currentUser?.email },
{
label: 'Linked tenants',
value: tenantSummaryValue,
},
]}
actions={
canViewOrganizations && organizationId
? [
{
href: getOrganizationViewHref(organizationId),
label: 'View organization',
color: 'info',
outline: true,
},
]
: []
}
helperText={
organizationId
? 'This is the workspace context attached to your account after signup and login.'
: 'Your account does not have an organization link yet.'
}
/>
<div className="space-y-3">
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-gray-500 dark:text-gray-300">
My tenant links
</p>
{isLoadingTenants ? (
<div className="rounded-xl border border-dashed border-blue-200 bg-blue-50/60 px-4 py-3 text-sm text-blue-900 dark:border-blue-900/70 dark:bg-blue-950/20 dark:text-blue-100">
Loading tenant context for your organization
</div>
) : linkedTenants.length ? (
<div className="space-y-3">
{linkedTenants.map((tenant) => (
<ConnectedEntityCard
key={tenant.id}
entityLabel="My tenant"
title={tenant.name || 'Unnamed tenant'}
badges={[<TenantStatusChip key={`${tenant.id}-status`} isActive={tenant.is_active} />]}
details={[
{ label: 'Slug', value: tenant.slug },
{ label: 'Domain', value: tenant.primary_domain },
{ label: 'Timezone', value: tenant.timezone },
{ label: 'Currency', value: tenant.default_currency },
]}
actions={
canViewTenants && tenant.id
? [
{
href: getTenantViewHref(tenant.id, organizationId, organizationName),
label: 'View tenant',
color: 'info',
outline: true,
},
]
: []
}
helperText="This tenant link is part of the organization context attached to your account."
/>
))}
</div>
) : (
<div className="rounded-xl border border-dashed border-gray-200 bg-gray-50 px-4 py-3 text-sm text-gray-600 dark:border-dark-700 dark:bg-dark-800 dark:text-gray-300">
{tenantEmptyStateMessage}
</div>
)}
</div>
</div>
</CardBox>
)
}
export default MyOrgTenantSummary