204 lines
6.9 KiB
TypeScript
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
|