Fine tuned organisation onboarding and tenant management

This commit is contained in:
2026-10-04 10:34:59 +02:00
parent 5fb67df9ac
commit 681ed99fea
5 changed files with 407 additions and 261 deletions
@@ -94,6 +94,8 @@ const organisationSchema = z.object({
name: z.string().min(3, 'Name must be at least 3 characters'),
registrationNo: z.string().min(4, 'Registration number is mandatory'),
email: z.string().email('Enter a valid email address'),
idNumber: z.string().min(4, 'Government issued ID number is required'),
organisationUser: z.string().min(1, 'Please select a linked user'),
type: z.coerce.number().min(1).max(27),
status: z.coerce.number().optional(),
});
@@ -422,6 +424,8 @@ export const GovernancePage: React.FC = () => {
name: '',
registrationNo: '',
email: '',
idNumber: '',
organisationUser: '',
type: OrganisationType.Enterprise,
status: ActivityStatus.Active,
},
@@ -456,11 +460,20 @@ export const GovernancePage: React.FC = () => {
name: data.name,
registrationNo: data.registrationNo,
email: data.email,
idNumber: data.idNumber,
organisationUser: data.organisationUser,
type: Number(data.type),
status: data.status !== undefined ? Number(data.status) : undefined,
status: data.status !== undefined ? Number(data.status) : ActivityStatus.Active,
});
}
return createOrganisation(data as CreateOrganisationRequest);
return createOrganisation({
name: data.name,
registrationNo: data.registrationNo,
email: data.email,
idNumber: data.idNumber,
organisationUser: data.organisationUser,
type: Number(data.type),
});
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['organisations'] });
@@ -537,10 +550,14 @@ export const GovernancePage: React.FC = () => {
const openCreateOrg = () => {
setEditingOrg(null);
const defaultUserId = usersDirectory[0]?.id || sub || currentUserId;
const defaultUser = usersDirectory.find((u) => u.id === defaultUserId);
orgForm.reset({
name: '',
registrationNo: '',
email: '',
idNumber: defaultUser?.idNumber || '',
organisationUser: defaultUserId,
type: OrganisationType.Enterprise,
status: ActivityStatus.Active,
});
@@ -549,10 +566,14 @@ export const GovernancePage: React.FC = () => {
const openEditOrg = (org: Organisation) => {
setEditingOrg(org);
const linkedUserId = org.userId || org.createdBy || usersDirectory[0]?.id || currentUserId;
const matchedUser = usersDirectory.find((u) => u.id === linkedUserId);
orgForm.reset({
name: org.name || '',
registrationNo: org.registrationNo || '',
email: org.email || '',
idNumber: matchedUser?.idNumber || (org as any).idNumber || '',
organisationUser: linkedUserId,
type: org.type || OrganisationType.Enterprise,
status: org.status ?? ActivityStatus.Active,
});
@@ -1412,6 +1433,65 @@ export const GovernancePage: React.FC = () => {
</div>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
<div>
<div className="flex items-center gap-1 mb-1">
<label className="block text-xs font-semibold text-slate-300">
Linked User Account *
</label>
<HelpTooltip
title="Designated Organisation User"
content="Select the designated user account associated with this organisation profile."
size="sm"
placement="top"
/>
</div>
<select
{...orgForm.register('organisationUser', {
onChange: (e) => {
const selected = usersDirectory.find((u) => u.id === e.target.value);
if (selected?.idNumber && !orgForm.getValues('idNumber')) {
orgForm.setValue('idNumber', selected.idNumber);
}
},
})}
className="w-full px-3 py-2 text-xs border border-slate-700/80 rounded-xl focus:outline-none focus:ring-1 focus:ring-blue-500 bg-[#090d16] text-white font-medium"
>
{usersDirectory.map((u) => (
<option key={u.id} value={u.id}>
{u.email || u.id} {u.role ? `(${u.role})` : ''} {u.idNumber ? `• ID: ${u.idNumber}` : ''}
</option>
))}
</select>
{orgForm.formState.errors.organisationUser && (
<p className="text-[10px] text-rose-500 mt-1">{orgForm.formState.errors.organisationUser.message}</p>
)}
</div>
<div>
<div className="flex items-center gap-1 mb-1">
<label className="block text-xs font-semibold text-slate-300">
Government Issued ID Number *
</label>
<HelpTooltip
title="Government Issued ID"
content="National identification document number or statutory passport ID of the linked user."
size="sm"
placement="top"
/>
</div>
<input
type="text"
{...orgForm.register('idNumber')}
className="w-full px-3 py-2 text-xs border border-slate-700/80 rounded-xl focus:outline-none focus:ring-1 focus:ring-blue-500 font-mono text-white bg-[#090d16]"
placeholder="e.g. 8401015800085 or Passport Number"
/>
{orgForm.formState.errors.idNumber && (
<p className="text-[10px] text-rose-500 mt-1">{orgForm.formState.errors.idNumber.message}</p>
)}
</div>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
<div>
<div className="flex items-center gap-1 mb-1">
@@ -2103,6 +2183,12 @@ export const GovernancePage: React.FC = () => {
<span className="text-slate-400 font-semibold text-[11px] uppercase tracking-wider shrink-0">Entity Type:</span>
<span className="text-slate-300">{getOrganisationTypeLabel(orgFreshDetail?.type ?? selectedOrgForDetail?.type ?? OrganisationType.Enterprise)}</span>
</div>
<div className="flex items-center gap-2 min-w-0">
<span className="text-slate-400 font-semibold text-[11px] uppercase tracking-wider shrink-0">Linked User:</span>
<span className="text-blue-400 font-mono text-[11px] truncate">
{getUserEmail(orgFreshDetail?.userId || selectedOrgForDetail?.userId) || orgFreshDetail?.userId || selectedOrgForDetail?.userId || '—'}
</span>
</div>
</div>
</div>
@@ -441,6 +441,7 @@ export const SettingsPage: React.FC = () => {
const userExportColumns: ExportColumn<User>[] = [
{ header: 'User Email', key: 'email', format: (val, u) => val || getUserEmail(u.id) || '—' },
{ header: 'User Identifier', key: 'id' },
{ header: 'Id Number', key: 'idNumber', format: (val) => val || '—' },
{ header: 'Role', key: 'role', format: (val) => getUserRoleBadge(val).label },
{ header: 'Status', key: 'status', format: (val) => getActivityStatusBadge(val).label },
{ header: 'Last Login', key: 'lastLoginAt', format: (val) => (val ? formatDate(val) : 'Never') },
@@ -770,6 +771,9 @@ export const SettingsPage: React.FC = () => {
<SortableTh column="id" currentColumn={userSortCol} direction={userSortDir} onSort={handleUserSort} className="py-3.5 px-4">
User Identifier
</SortableTh>
<SortableTh column="idNumber" currentColumn={userSortCol} direction={userSortDir} onSort={handleUserSort} className="py-3.5 px-4">
Id Number
</SortableTh>
<SortableTh column="role" currentColumn={userSortCol} direction={userSortDir} onSort={handleUserSort} className="py-3.5 px-4">
Role
</SortableTh>
@@ -785,13 +789,13 @@ export const SettingsPage: React.FC = () => {
<tbody className="divide-y divide-slate-800/50">
{loadingUsers ? (
<tr>
<td colSpan={6} className="py-8 text-center text-slate-500">
<td colSpan={7} className="py-8 text-center text-slate-500">
Loading user directory...
</td>
</tr>
) : sortedUsers.length === 0 ? (
<tr>
<td colSpan={6} className="py-8 text-center text-slate-500">
<td colSpan={7} className="py-8 text-center text-slate-500">
No users found matching current filters.
</td>
</tr>
@@ -806,6 +810,9 @@ export const SettingsPage: React.FC = () => {
<td className="py-3.5 px-4 font-mono text-slate-400 text-[11px]">
{u.id}
</td>
<td className="py-3.5 px-4 font-mono text-slate-300 text-[11px]">
{u.idNumber || <span className="text-slate-600">—</span>}
</td>
<td className="py-3.5 px-4">
<UserRoleBadge role={u.role} />
</td>
@@ -2113,6 +2120,12 @@ export const SettingsPage: React.FC = () => {
{userFreshDetail?.id || selectedUserForDetail.id}
</span>
</div>
<div>
<span className="text-slate-400 font-semibold block text-[11px] uppercase">Id Number:</span>
<span className="font-mono text-slate-200 text-xs">
{userFreshDetail?.idNumber || selectedUserForDetail.idNumber || '—'}
</span>
</div>
<div>
<span className="text-slate-400 font-semibold block text-[11px] uppercase mb-1">System Role:</span>
<UserRoleBadge role={userFreshDetail?.role ?? selectedUserForDetail.role} />
@@ -13,21 +13,19 @@ import {
Building2,
Layers,
Info,
Copy,
Check,
Eye,
AlertTriangle,
Lock,
ChevronDown,
ChevronRight,
Mail,
Shield,
User as UserIcon,
} from 'lucide-react';
import {
getTenants,
createTenant,
updateTenant,
getOrganisations,
getTenantOrganisations,
} from '../services/api';
import { Modal } from '../components/ui/Modal';
import { useToast } from '../components/ui/Toast';
@@ -36,9 +34,8 @@ import { ExportButton } from '../components/ui/ExportButton';
import { ExportColumn } from '../utils/exportUtils';
import { useTableSort, SortableTh } from '../utils/tableSort';
import { ActivityStatusBadge } from '../components/ui/StatusBadge';
import { Tenant, Organisation, ActivityStatus, CreateTenantRequest, UpdateTenantRequest } from '../types';
import { formatDate, getOrganisationTypeLabel } from '../lib/formatters';
import { encodeHashIdSync } from '../services/toolsService';
import { Tenant, TenantOrganisation, ActivityStatus, CreateTenantRequest, UpdateTenantRequest } from '../types';
import { formatDate, getOrganisationTypeLabel, getUserEmail } from '../lib/formatters';
import { useAuth } from '../auth/AuthProvider';
const createTenantSchema = z.object({
@@ -53,14 +50,188 @@ const updateTenantSchema = z.object({
type CreateTenantFormValues = z.infer<typeof createTenantSchema>;
type UpdateTenantFormValues = z.infer<typeof updateTenantSchema>;
// Sub-component for rendering tenant organisations in expanded row
const TenantOrganisationsSubList: React.FC<{ tenantId: number | string; tenantName?: string | null }> = ({
tenantId,
tenantName,
}) => {
const {
data: tenantOrganisations = [],
isLoading,
isError,
refetch,
} = useQuery<TenantOrganisation[]>({
queryKey: ['tenant-organisations', tenantId],
queryFn: () => getTenantOrganisations(tenantId),
staleTime: 30000,
});
return (
<div className="py-4 px-6 border-l-4 border-blue-500 bg-linear-to-r from-blue-950/20 via-slate-900/40 to-slate-950/60 space-y-3">
{/* Sub-list Header */}
<div className="flex items-center justify-between flex-wrap gap-2">
<div className="flex items-center gap-2">
<div className="p-1.5 rounded-lg bg-blue-500/10 text-blue-400 border border-blue-500/20">
<Building2 className="w-4 h-4" />
</div>
<div>
<div className="flex items-center gap-2">
<h4 className="text-xs font-bold text-white tracking-tight">
Scoped Organisations for {tenantName || `Tenant #${tenantId}`}
</h4>
<span className="text-[10px] font-semibold text-blue-300 bg-blue-500/20 px-2 py-0.5 rounded-full border border-blue-500/30">
{tenantOrganisations.length} Legal {tenantOrganisations.length === 1 ? 'Entity' : 'Entities'}
</span>
</div>
<p className="text-[11px] text-slate-400">
Legal entities and holding structures mapped under Tenant ID #{tenantId} partition boundary (Read-Only).
</p>
</div>
</div>
<div className="flex items-center gap-2">
<button
onClick={() => refetch()}
className="p-1 text-slate-400 hover:text-white rounded hover:bg-slate-800 transition cursor-pointer"
title="Refresh organisations list"
>
<RefreshCw className={`w-3.5 h-3.5 ${isLoading ? 'animate-spin text-blue-400' : ''}`} />
</button>
<div className="text-[11px] text-slate-500 italic">
Organisation management is handled in Governance &amp; Structure
</div>
</div>
</div>
{/* Organisations Sub-Table */}
{isLoading ? (
<div className="py-6 flex items-center justify-center gap-2 text-slate-400 text-xs">
<RefreshCw className="w-4 h-4 animate-spin text-blue-400" />
<span>Loading partition organisations from server...</span>
</div>
) : isError ? (
<div className="py-4 px-4 rounded-lg bg-rose-950/30 border border-rose-900/40 text-center">
<p className="text-xs text-rose-300">Unable to retrieve organisations for Tenant #{tenantId}.</p>
<button
onClick={() => refetch()}
className="mt-1 text-[11px] text-blue-400 hover:underline cursor-pointer"
>
Retry Query
</button>
</div>
) : tenantOrganisations.length === 0 ? (
<div className="py-6 px-4 rounded-lg bg-slate-900/80 border border-slate-800 text-center">
<Building2 className="w-5 h-5 text-slate-600 mx-auto mb-1.5" />
<p className="text-xs font-medium text-slate-300">No organisations assigned to Tenant #{tenantId}</p>
<p className="text-[11px] text-slate-500 mt-0.5">
New legal entities provisioned with Tenant ID #{tenantId} will automatically appear in this partition view.
</p>
</div>
) : (
<div className="rounded-lg border border-slate-800/80 overflow-hidden bg-slate-900/90 shadow-xs">
<table className="w-full text-left text-xs border-collapse">
<thead>
<tr className="bg-slate-950/90 border-b border-slate-800/80 text-slate-400 font-medium">
<th className="py-2.5 px-3 font-semibold">Org ID</th>
<th className="py-2.5 px-3 font-semibold">Organisation Name</th>
<th className="py-2.5 px-3 font-semibold">Registration No.</th>
<th className="py-2.5 px-3 font-semibold">Entity Type</th>
<th className="py-2.5 px-3 font-semibold">Contact Email</th>
<th className="py-2.5 px-3 font-semibold">Linked User Email</th>
<th className="py-2.5 px-3 font-semibold">Status</th>
<th className="py-2.5 px-3 font-semibold text-right">Created Date</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-800/50">
{tenantOrganisations.map((tOrg) => {
const org = tOrg.organisation;
const orgId = org?.id || tOrg.organisationId || tOrg.id;
const orgName = org?.name || 'Unnamed Organisation';
const regNo = org?.registrationNo || '—';
const orgType = org?.type;
const orgEmail = org?.email || '—';
const userEmailDisplay =
tOrg.creator?.email ||
getUserEmail(org?.userId) ||
getUserEmail(tOrg.createdBy) ||
org?.userId ||
'—';
const status = org?.status ?? tOrg.status;
const createdAt = tOrg.createdAt || org?.createdAt;
return (
<tr key={String(tOrg.id) + '-' + String(orgId)} className="hover:bg-slate-800/30 transition">
{/* Integer Org ID only (no hashid) */}
<td className="py-2.5 px-3 font-mono">
<span className="text-slate-300 font-semibold">#{orgId}</span>
</td>
{/* Organisation Name */}
<td className="py-2.5 px-3">
<span className="font-medium text-white">{orgName}</span>
</td>
{/* Registration No */}
<td className="py-2.5 px-3 text-slate-400 font-mono text-[11px]">
{regNo}
</td>
{/* Entity Type */}
<td className="py-2.5 px-3 text-slate-300">
<span className="bg-slate-800/80 text-slate-300 px-2 py-0.5 rounded border border-slate-700/60 text-[11px]">
{orgType !== undefined ? getOrganisationTypeLabel(orgType) : 'Organisation'}
</span>
</td>
{/* Contact Email */}
<td className="py-2.5 px-3 text-slate-300">
{orgEmail && orgEmail !== '—' ? (
<div className="flex items-center gap-1.5 text-[11px] text-slate-300">
<Mail className="w-3 h-3 text-slate-500 shrink-0" />
<span className="truncate max-w-[160px]">{orgEmail}</span>
</div>
) : (
<span className="text-slate-500">—</span>
)}
</td>
{/* Linked User (swapped raw ID for email) */}
<td className="py-2.5 px-3 text-slate-300">
<div className="flex items-center gap-1.5 text-[11px] text-blue-300 font-mono">
<UserIcon className="w-3 h-3 text-blue-400 shrink-0" />
<span className="truncate max-w-[180px]" title={userEmailDisplay}>
{userEmailDisplay}
</span>
</div>
</td>
{/* Status */}
<td className="py-2.5 px-3">
<ActivityStatusBadge status={status} />
</td>
{/* Created Date */}
<td className="py-2.5 px-3 text-slate-400 text-right">
{formatDate(createdAt)}
</td>
</tr>
);
})}
</tbody>
</table>
</div>
)}
</div>
);
};
export const TenantManagementPage: React.FC = () => {
const queryClient = useQueryClient();
const { success, error: toastError } = useToast();
const { tenantId: currentTenantId, tenantName: currentTenantName, hashedTenantId: currentHashedTenantId } = useAuth();
const { tenantId: currentTenantId, tenantName: currentTenantName } = useAuth();
const [searchTerm, setSearchTerm] = useState('');
const [statusFilter, setStatusFilter] = useState<string>('all');
const [copiedId, setCopiedId] = useState<string | null>(null);
const [expandedTenants, setExpandedTenants] = useState<Record<string, boolean>>({});
// Modals
@@ -81,18 +252,6 @@ export const TenantManagementPage: React.FC = () => {
staleTime: 30000,
});
// Query Organisations to populate tenant sub-lists
const {
data: organisations = [],
isLoading: loadingOrgs,
isFetching: fetchingOrgs,
refetch: refetchOrgs,
} = useQuery<Organisation[]>({
queryKey: ['organisations'],
queryFn: () => getOrganisations(),
staleTime: 60000,
});
// Forms
const createForm = useForm<CreateTenantFormValues>({
resolver: zodResolver(createTenantSchema),
@@ -152,12 +311,6 @@ export const TenantManagementPage: React.FC = () => {
setIsDetailModalOpen(true);
};
const handleCopyHashId = (text: string, idKey: string) => {
navigator.clipboard.writeText(text);
setCopiedId(idKey);
setTimeout(() => setCopiedId(null), 2000);
};
const toggleTenantExpand = (id: string | number) => {
const key = String(id);
setExpandedTenants((prev) => ({
@@ -182,7 +335,6 @@ export const TenantManagementPage: React.FC = () => {
!searchTerm.trim() ||
(t.name || '').toLowerCase().includes(searchTerm.toLowerCase()) ||
String(t.id).includes(searchTerm) ||
encodeHashIdSync(t.id).toLowerCase().includes(searchTerm.toLowerCase()) ||
(t.createdBy || '').toLowerCase().includes(searchTerm.toLowerCase());
const matchesStatus =
@@ -204,10 +356,9 @@ export const TenantManagementPage: React.FC = () => {
const inactiveCount = tenants.filter((t) => Number(t.status) === 2).length;
const disabledCount = tenants.filter((t) => Number(t.status) === 3).length;
// Export columns
// Export columns (using integer ID only, no hashid)
const exportColumns: ExportColumn<Tenant>[] = [
{ header: 'Tenant ID', key: 'id' },
{ header: 'Partition HashID', key: 'id', format: (val) => encodeHashIdSync(val) },
{ header: 'Tenant Name', key: 'name', format: (val) => val || '—' },
{ header: 'Operational Status', key: 'status', format: (val) => getStatusString(val) },
{ header: 'Created By', key: 'createdBy', format: (val) => val || '—' },
@@ -217,51 +368,63 @@ export const TenantManagementPage: React.FC = () => {
const handleRefreshAll = () => {
refetchTenants();
refetchOrgs();
queryClient.invalidateQueries({ queryKey: ['tenant-organisations'] });
};
return (
<div className="space-y-6 pb-12">
{/* Top Header */}
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4 border-b border-slate-800/80 pb-5">
<div>
<div className="flex items-center gap-2.5 mb-1">
<div className="p-2 rounded-xl bg-blue-500/10 border border-blue-500/20 text-blue-400">
<Database className="w-5 h-5" />
<div className="space-y-6 max-w-7xl mx-auto pb-12">
{/* Header Banner - Conforming to standard page design */}
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4 bg-gradient-to-r from-[#0d1322] via-[#0f172a] to-[#0d1322] p-6 sm:p-8 rounded-3xl border border-slate-800/80 shadow-2xl relative overflow-hidden">
<div className="absolute -right-10 -bottom-10 w-60 h-60 bg-blue-600/10 rounded-full blur-3xl pointer-events-none" />
<div className="space-y-2 relative z-10">
<div className="flex items-center gap-3">
<div className="h-12 w-12 rounded-2xl bg-blue-600/20 border border-blue-500/30 flex items-center justify-center text-blue-400 shadow-lg shadow-blue-500/10">
<Database className="w-6 h-6" />
</div>
<div>
<div className="flex items-center gap-2.5 flex-wrap">
<h1 className="text-2xl sm:text-3xl font-bold tracking-tight text-white flex items-center gap-2.5">
Tenant Management
</h1>
<span className="text-[11px] font-semibold text-blue-400 bg-blue-500/10 px-2.5 py-0.5 rounded-full border border-blue-500/30">
Directory
</span>
<HelpTooltip
title="Multi-Tenancy & Partition Governance"
content="Govern database partition boundaries, manage statutory authorities, and control operational tenancy lifecycle status."
placement="right"
/>
</div>
<p className="text-xs sm:text-sm text-slate-400 font-normal mt-0.5">
Manage multi-tenant isolated partitions, statutory boundaries, and enterprise organizational tenancy.
</p>
</div>
<h1 className="text-xl font-bold text-white tracking-tight">Tenant Management</h1>
<span className="text-[11px] font-semibold text-blue-400 bg-blue-500/10 px-2.5 py-0.5 rounded-full border border-blue-500/20">
Multi-Tenancy
</span>
</div>
<p className="text-xs text-slate-400 max-w-2xl">
Control tenant database partition boundaries, onboard statutory authorities, and govern tenant lifecycle status.
Expand any tenant row to inspect the scoped organisations governed under that partition.
</p>
</div>
<div className="flex items-center gap-2.5 self-start sm:self-auto">
<div className="flex items-center gap-2.5 relative z-10 self-start md:self-center flex-wrap">
<button
onClick={handleRefreshAll}
disabled={fetchingTenants || fetchingOrgs}
className="flex items-center gap-1.5 px-3 py-2 bg-slate-900/80 hover:bg-slate-800 text-slate-300 text-xs font-medium rounded-lg border border-slate-800 transition shadow-xs disabled:opacity-50 cursor-pointer"
title="Refresh tenants directory and organisations"
disabled={fetchingTenants}
className="inline-flex items-center justify-center gap-1.5 px-3.5 py-2 text-xs font-semibold text-slate-300 bg-[#0d121f] hover:bg-slate-800 border border-slate-700/80 rounded-xl transition cursor-pointer shadow-xs disabled:opacity-50"
title="Refresh tenants directory"
>
<RefreshCw className={`w-3.5 h-3.5 ${fetchingTenants || fetchingOrgs ? 'animate-spin text-blue-400' : ''}`} />
<RefreshCw className={`w-3.5 h-3.5 text-blue-400 ${fetchingTenants ? 'animate-spin' : ''}`} />
<span>Refresh</span>
</button>
<ExportButton
title="PFM Multi-Tenant Directory Register"
subtitle="Official directory of registered tenant partitions"
filename={`pfm-tenants-${new Date().toISOString().split('T')[0]}`}
data={sortedData}
columns={exportColumns}
filename="PFM_Tenants_Directory"
label="Export Tenants"
/>
<button
onClick={handleOpenCreate}
className="flex items-center gap-1.5 px-3.5 py-2 bg-blue-600 hover:bg-blue-500 text-white text-xs font-semibold rounded-lg shadow-sm shadow-blue-600/30 transition cursor-pointer"
className="inline-flex items-center justify-center gap-1.5 px-4 py-2 text-xs font-semibold text-white bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-500 hover:to-indigo-500 rounded-xl shadow-md shadow-blue-500/20 transition cursor-pointer"
>
<Plus className="w-4 h-4" />
<span>New Tenant</span>
@@ -272,7 +435,7 @@ export const TenantManagementPage: React.FC = () => {
{/* Summary KPI Cards */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
{/* Total Tenants */}
<div className="p-4 rounded-xl bg-slate-900/60 border border-slate-800 shadow-sm flex items-center justify-between">
<div className="p-4 rounded-2xl bg-[#0f1523]/80 border border-slate-800/80 shadow-md flex items-center justify-between backdrop-blur-md">
<div>
<p className="text-xs font-medium text-slate-400">Total Partitions</p>
<p className="text-2xl font-bold text-white mt-1">{loadingTenants ? '—' : totalCount}</p>
@@ -284,7 +447,7 @@ export const TenantManagementPage: React.FC = () => {
</div>
{/* Active Tenants */}
<div className="p-4 rounded-xl bg-slate-900/60 border border-slate-800 shadow-sm flex items-center justify-between">
<div className="p-4 rounded-2xl bg-[#0f1523]/80 border border-slate-800/80 shadow-md flex items-center justify-between backdrop-blur-md">
<div>
<p className="text-xs font-medium text-slate-400">Active Tenants</p>
<p className="text-2xl font-bold text-emerald-400 mt-1">{loadingTenants ? '—' : activeCount}</p>
@@ -296,7 +459,7 @@ export const TenantManagementPage: React.FC = () => {
</div>
{/* Inactive / Disabled Tenants */}
<div className="p-4 rounded-xl bg-slate-900/60 border border-slate-800 shadow-sm flex items-center justify-between">
<div className="p-4 rounded-2xl bg-[#0f1523]/80 border border-slate-800/80 shadow-md flex items-center justify-between backdrop-blur-md">
<div>
<p className="text-xs font-medium text-slate-400">Locked / Inactive</p>
<p className="text-2xl font-bold text-rose-400 mt-1">{loadingTenants ? '—' : inactiveCount + disabledCount}</p>
@@ -308,7 +471,7 @@ export const TenantManagementPage: React.FC = () => {
</div>
{/* Current Active Tenant */}
<div className="p-4 rounded-xl bg-blue-950/20 border border-blue-800/40 shadow-sm flex items-center justify-between">
<div className="p-4 rounded-2xl bg-blue-950/20 border border-blue-800/40 shadow-md flex items-center justify-between backdrop-blur-md">
<div className="min-w-0">
<p className="text-xs font-medium text-blue-300 flex items-center gap-1.5">
<span>Your Active Partition</span>
@@ -321,8 +484,8 @@ export const TenantManagementPage: React.FC = () => {
<p className="text-sm font-bold text-white mt-1 truncate" title={currentTenantName || 'Default Tenant'}>
{currentTenantName || `Tenant #${currentTenantId || 1}`}
</p>
<p className="text-[11px] font-mono text-blue-400 mt-0.5 truncate">
HashID: {currentHashedTenantId || '—'}
<p className="text-[11px] text-blue-400 font-mono mt-0.5">
Tenant ID: #{currentTenantId || 1}
</p>
</div>
<div className="p-3 rounded-xl bg-blue-900/40 text-blue-400 border border-blue-700/50 shrink-0">
@@ -332,23 +495,23 @@ export const TenantManagementPage: React.FC = () => {
</div>
{/* Search & Filter Bar */}
<div className="flex flex-col md:flex-row items-stretch md:items-center justify-between gap-3 bg-slate-900/80 p-3 rounded-xl border border-slate-800/80">
<div className="flex flex-col md:flex-row items-stretch md:items-center justify-between gap-3 bg-[#0f1523]/80 p-3 rounded-2xl border border-slate-800/80 backdrop-blur-md">
<div className="relative flex-1">
<Search className="w-4 h-4 text-slate-400 absolute left-3 top-1/2 -translate-y-1/2" />
<input
type="text"
placeholder="Search by Tenant Name, ID, HashID, or Creator..."
placeholder="Search by Tenant Name, ID, or Creator..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="w-full pl-9 pr-4 py-2 bg-slate-950/80 border border-slate-800 rounded-lg text-xs text-slate-200 placeholder-slate-500 focus:outline-hidden focus:border-blue-500 focus:ring-1 focus:ring-blue-500"
className="w-full pl-9 pr-4 py-2 bg-slate-950/80 border border-slate-800 rounded-xl text-xs text-slate-200 placeholder-slate-500 focus:outline-hidden focus:border-blue-500 focus:ring-1 focus:ring-blue-500"
/>
</div>
<div className="flex items-center gap-2 shrink-0">
<div className="flex items-center bg-slate-950/80 border border-slate-800 rounded-lg p-1 text-xs">
<div className="flex items-center bg-slate-950/80 border border-slate-800 rounded-xl p-1 text-xs">
<button
onClick={() => setStatusFilter('all')}
className={`px-3 py-1 rounded-md transition font-medium cursor-pointer ${
className={`px-3 py-1 rounded-lg transition font-medium cursor-pointer ${
statusFilter === 'all'
? 'bg-blue-600 text-white shadow-xs'
: 'text-slate-400 hover:text-slate-200'
@@ -358,7 +521,7 @@ export const TenantManagementPage: React.FC = () => {
</button>
<button
onClick={() => setStatusFilter('active')}
className={`px-3 py-1 rounded-md transition font-medium cursor-pointer ${
className={`px-3 py-1 rounded-lg transition font-medium cursor-pointer ${
statusFilter === 'active'
? 'bg-emerald-600 text-white shadow-xs'
: 'text-slate-400 hover:text-slate-200'
@@ -368,7 +531,7 @@ export const TenantManagementPage: React.FC = () => {
</button>
<button
onClick={() => setStatusFilter('inactive')}
className={`px-3 py-1 rounded-md transition font-medium cursor-pointer ${
className={`px-3 py-1 rounded-lg transition font-medium cursor-pointer ${
statusFilter === 'inactive'
? 'bg-amber-600 text-white shadow-xs'
: 'text-slate-400 hover:text-slate-200'
@@ -378,7 +541,7 @@ export const TenantManagementPage: React.FC = () => {
</button>
<button
onClick={() => setStatusFilter('disabled')}
className={`px-3 py-1 rounded-md transition font-medium cursor-pointer ${
className={`px-3 py-1 rounded-lg transition font-medium cursor-pointer ${
statusFilter === 'disabled'
? 'bg-rose-600 text-white shadow-xs'
: 'text-slate-400 hover:text-slate-200'
@@ -391,16 +554,16 @@ export const TenantManagementPage: React.FC = () => {
</div>
{/* Tenants Table */}
<div className="bg-slate-900/60 rounded-xl border border-slate-800 overflow-hidden shadow-sm">
<div className="bg-[#0f1523]/80 rounded-2xl border border-slate-800/80 overflow-hidden shadow-xl backdrop-blur-md">
<div className="overflow-x-auto">
<table className="w-full text-left text-xs border-collapse">
<thead>
<tr className="bg-slate-950/80 border-b border-slate-800 text-slate-400 font-semibold uppercase tracking-wider">
<tr className="bg-slate-900/60 border-b border-slate-800/80 text-slate-400 font-semibold uppercase tracking-wider">
<th className="py-3.5 px-3 w-10 text-center">
<span className="sr-only">Expand</span>
</th>
<SortableTh column="id" currentColumn={sortColumn} direction={sortDirection} onSort={handleSort} className="py-3.5 px-4">
Tenant ID & HashID
Tenant ID
</SortableTh>
<SortableTh column="name" currentColumn={sortColumn} direction={sortDirection} onSort={handleSort} className="py-3.5 px-4">
Tenant Organization Name
@@ -441,19 +604,11 @@ export const TenantManagementPage: React.FC = () => {
</tr>
) : (
sortedData.map((tenant) => {
const hashId = encodeHashIdSync(tenant.id);
const isCurrent = String(tenant.id) === String(currentTenantId);
const s = Number(tenant.status ?? ActivityStatus.Active);
const isActive = s === 1 || s === ActivityStatus.Active;
const isExpanded = !!expandedTenants[String(tenant.id)];
// Filter organisations matching this tenant
const tenantOrganisations = organisations.filter(
(org) =>
(org.tenantId !== undefined && org.tenantId !== null && String(org.tenantId) === String(tenant.id)) ||
((org as any).tenant_id !== undefined && (org as any).tenant_id !== null && String((org as any).tenant_id) === String(tenant.id)) ||
String(org.id) === String(tenant.id)
);
const creatorEmail = getUserEmail(tenant.createdBy) || tenant.createdBy || 'System Administrator';
return (
<React.Fragment key={tenant.id}>
@@ -477,28 +632,12 @@ export const TenantManagementPage: React.FC = () => {
</button>
</td>
{/* ID & HashID */}
<td className="py-3 px-4">
<div className="flex items-center gap-2">
<span className="font-semibold text-white">#{tenant.id}</span>
<span className="text-[10px] font-mono text-blue-400 bg-blue-950/60 px-1.5 py-0.5 rounded border border-blue-800/50">
{hashId}
</span>
<button
onClick={() => handleCopyHashId(hashId, String(tenant.id))}
className="text-slate-500 hover:text-slate-300 transition cursor-pointer"
title="Copy HashID"
>
{copiedId === String(tenant.id) ? (
<Check className="w-3.5 h-3.5 text-emerald-400" />
) : (
<Copy className="w-3.5 h-3.5" />
)}
</button>
</div>
{/* Integer Tenant ID only (no hashid) */}
<td className="py-3 px-4 font-mono">
<span className="font-semibold text-white">#{tenant.id}</span>
</td>
{/* Tenant Name & Sub-Organisations Count */}
{/* Tenant Name */}
<td className="py-3 px-4">
<div className="flex items-center gap-2">
<Building2 className="w-4 h-4 text-slate-400 shrink-0" />
@@ -508,13 +647,6 @@ export const TenantManagementPage: React.FC = () => {
Active Context
</span>
)}
<button
onClick={() => toggleTenantExpand(tenant.id)}
className="text-[10px] font-medium text-slate-400 hover:text-blue-300 bg-slate-800/80 hover:bg-slate-700/80 px-2 py-0.5 rounded-full border border-slate-700/60 transition cursor-pointer flex items-center gap-1"
title="Click to view scoped organisations"
>
<span>{tenantOrganisations.length} Org{tenantOrganisations.length === 1 ? '' : 's'}</span>
</button>
</div>
</td>
@@ -534,9 +666,11 @@ export const TenantManagementPage: React.FC = () => {
</div>
</td>
{/* Created By */}
{/* Created By (Swapped ID for Email where available) */}
<td className="py-3 px-4 text-slate-400 font-mono text-[11px]">
{tenant.createdBy || 'System Administrator'}
<span title={String(tenant.createdBy || '')}>
{creatorEmail}
</span>
</td>
{/* Created Date */}
@@ -566,126 +700,11 @@ export const TenantManagementPage: React.FC = () => {
</td>
</tr>
{/* Expanded Sub-List of Organisations */}
{/* Expanded Sub-List of Organisations using getTenantOrganisations */}
{isExpanded && (
<tr className="bg-slate-950/70 border-b border-slate-800/80">
<td colSpan={7} className="p-0">
<div className="py-4 px-6 border-l-4 border-blue-500 bg-linear-to-r from-blue-950/20 via-slate-900/40 to-slate-950/60 space-y-3">
{/* Sub-list Header */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<div className="p-1.5 rounded-lg bg-blue-500/10 text-blue-400 border border-blue-500/20">
<Building2 className="w-4 h-4" />
</div>
<div>
<div className="flex items-center gap-2">
<h4 className="text-xs font-bold text-white tracking-tight">
Scoped Organisations for {tenant.name || `Tenant #${tenant.id}`}
</h4>
<span className="text-[10px] font-semibold text-blue-300 bg-blue-500/20 px-2 py-0.5 rounded-full border border-blue-500/30">
{tenantOrganisations.length} Legal {tenantOrganisations.length === 1 ? 'Entity' : 'Entities'}
</span>
</div>
<p className="text-[11px] text-slate-400">
Legal entities and holding structures mapped under Tenant ID #{tenant.id} partition boundary (Read-Only).
</p>
</div>
</div>
<div className="text-[11px] text-slate-500 italic">
Organisation management is handled in Governance & Structure
</div>
</div>
{/* Organisations Sub-Table */}
{loadingOrgs ? (
<div className="py-6 flex items-center justify-center gap-2 text-slate-400 text-xs">
<RefreshCw className="w-4 h-4 animate-spin text-blue-400" />
<span>Loading organisations...</span>
</div>
) : tenantOrganisations.length === 0 ? (
<div className="py-6 px-4 rounded-lg bg-slate-900/80 border border-slate-800 text-center">
<Building2 className="w-5 h-5 text-slate-600 mx-auto mb-1.5" />
<p className="text-xs font-medium text-slate-300">No organisations assigned to this tenant</p>
<p className="text-[11px] text-slate-500 mt-0.5">
New organisations provisioned with Tenant ID #{tenant.id} will automatically appear in this partition view.
</p>
</div>
) : (
<div className="rounded-lg border border-slate-800/80 overflow-hidden bg-slate-900/90 shadow-xs">
<table className="w-full text-left text-xs border-collapse">
<thead>
<tr className="bg-slate-950/90 border-b border-slate-800/80 text-slate-400 font-medium">
<th className="py-2.5 px-3 font-semibold">Org ID & HashID</th>
<th className="py-2.5 px-3 font-semibold">Organisation Name</th>
<th className="py-2.5 px-3 font-semibold">Registration No.</th>
<th className="py-2.5 px-3 font-semibold">Entity Type</th>
<th className="py-2.5 px-3 font-semibold">Official Contact</th>
<th className="py-2.5 px-3 font-semibold">Status</th>
<th className="py-2.5 px-3 font-semibold text-right">Created Date</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-800/50">
{tenantOrganisations.map((org) => {
const orgHashId = encodeHashIdSync(org.id);
return (
<tr key={org.id} className="hover:bg-slate-800/30 transition">
{/* ID & HashID */}
<td className="py-2.5 px-3 font-mono">
<div className="flex items-center gap-1.5">
<span className="text-slate-300 font-semibold">#{org.id}</span>
<span className="text-[10px] text-blue-400 bg-blue-950/50 px-1.5 py-0.5 rounded border border-blue-800/40">
{orgHashId}
</span>
</div>
</td>
{/* Name */}
<td className="py-2.5 px-3">
<span className="font-medium text-white">{org.name || 'Unnamed Organisation'}</span>
</td>
{/* Registration No */}
<td className="py-2.5 px-3 text-slate-400 font-mono text-[11px]">
{org.registrationNo || '—'}
</td>
{/* Type */}
<td className="py-2.5 px-3 text-slate-300">
<span className="bg-slate-800/80 text-slate-300 px-2 py-0.5 rounded border border-slate-700/60 text-[11px]">
{getOrganisationTypeLabel(org.type)}
</span>
</td>
{/* Email */}
<td className="py-2.5 px-3 text-slate-300">
{org.email ? (
<div className="flex items-center gap-1.5 text-[11px] text-slate-300">
<Mail className="w-3 h-3 text-slate-500 shrink-0" />
<span className="truncate max-w-[180px]">{org.email}</span>
</div>
) : (
<span className="text-slate-500">—</span>
)}
</td>
{/* Status */}
<td className="py-2.5 px-3">
<ActivityStatusBadge status={org.status} />
</td>
{/* Created Date */}
<td className="py-2.5 px-3 text-slate-400 text-right">
{formatDate(org.createdAt)}
</td>
</tr>
);
})}
</tbody>
</table>
</div>
)}
</div>
<TenantOrganisationsSubList tenantId={tenant.id} tenantName={tenant.name} />
</td>
</tr>
)}
@@ -730,7 +749,7 @@ export const TenantManagementPage: React.FC = () => {
type="text"
placeholder="e.g. Technology Innovation Agency (TIA)"
{...createForm.register('name')}
className="w-full px-3.5 py-2.5 bg-slate-950/80 border border-slate-800 rounded-lg text-xs text-white placeholder-slate-500 focus:outline-hidden focus:border-blue-500 focus:ring-1 focus:ring-blue-500"
className="w-full px-3.5 py-2.5 bg-slate-950/80 border border-slate-800 rounded-xl text-xs text-white placeholder-slate-500 focus:outline-hidden focus:border-blue-500 focus:ring-1 focus:ring-blue-500"
/>
{createForm.formState.errors.name && (
<p className="text-[11px] text-rose-400 mt-1">{createForm.formState.errors.name.message}</p>
@@ -741,14 +760,14 @@ export const TenantManagementPage: React.FC = () => {
<button
type="button"
onClick={() => setIsCreateModalOpen(false)}
className="px-4 py-2 bg-slate-800 hover:bg-slate-700 text-slate-300 text-xs font-medium rounded-lg transition cursor-pointer"
className="px-4 py-2 bg-slate-800 hover:bg-slate-700 text-slate-300 text-xs font-medium rounded-xl transition cursor-pointer"
>
Cancel
</button>
<button
type="submit"
disabled={createMutation.isPending}
className="flex items-center gap-1.5 px-4 py-2 bg-blue-600 hover:bg-blue-500 text-white text-xs font-semibold rounded-lg shadow-sm transition disabled:opacity-50 cursor-pointer"
className="flex items-center gap-1.5 px-4 py-2 bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-500 hover:to-indigo-500 text-white text-xs font-semibold rounded-xl shadow-md transition disabled:opacity-50 cursor-pointer"
>
{createMutation.isPending && <RefreshCw className="w-3.5 h-3.5 animate-spin" />}
<span>{createMutation.isPending ? 'Provisioning...' : 'Create Tenant'}</span>
@@ -782,7 +801,7 @@ export const TenantManagementPage: React.FC = () => {
<input
type="text"
{...updateForm.register('name')}
className="w-full px-3.5 py-2.5 bg-slate-950/80 border border-slate-800 rounded-lg text-xs text-white placeholder-slate-500 focus:outline-hidden focus:border-blue-500 focus:ring-1 focus:ring-blue-500"
className="w-full px-3.5 py-2.5 bg-slate-950/80 border border-slate-800 rounded-xl text-xs text-white placeholder-slate-500 focus:outline-hidden focus:border-blue-500 focus:ring-1 focus:ring-blue-500"
/>
{updateForm.formState.errors.name && (
<p className="text-[11px] text-rose-400 mt-1">{updateForm.formState.errors.name.message}</p>
@@ -795,7 +814,7 @@ export const TenantManagementPage: React.FC = () => {
</label>
<select
{...updateForm.register('status')}
className="w-full px-3.5 py-2.5 bg-slate-950/80 border border-slate-800 rounded-lg text-xs text-white focus:outline-hidden focus:border-blue-500 focus:ring-1 focus:ring-blue-500"
className="w-full px-3.5 py-2.5 bg-slate-950/80 border border-slate-800 rounded-xl text-xs text-white focus:outline-hidden focus:border-blue-500 focus:ring-1 focus:ring-blue-500"
>
<option value={ActivityStatus.Active}>Active (Users Permitted)</option>
<option value={ActivityStatus.Inactive}>Inactive (Tenant Lockout)</option>
@@ -821,14 +840,14 @@ export const TenantManagementPage: React.FC = () => {
<button
type="button"
onClick={() => setIsUpdateModalOpen(false)}
className="px-4 py-2 bg-slate-800 hover:bg-slate-700 text-slate-300 text-xs font-medium rounded-lg transition cursor-pointer"
className="px-4 py-2 bg-slate-800 hover:bg-slate-700 text-slate-300 text-xs font-medium rounded-xl transition cursor-pointer"
>
Cancel
</button>
<button
type="submit"
disabled={updateMutation.isPending}
className="flex items-center gap-1.5 px-4 py-2 bg-blue-600 hover:bg-blue-500 text-white text-xs font-semibold rounded-lg shadow-sm transition disabled:opacity-50 cursor-pointer"
className="flex items-center gap-1.5 px-4 py-2 bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-500 hover:to-indigo-500 text-white text-xs font-semibold rounded-xl shadow-md transition disabled:opacity-50 cursor-pointer"
>
{updateMutation.isPending && <RefreshCw className="w-3.5 h-3.5 animate-spin" />}
<span>{updateMutation.isPending ? 'Saving...' : 'Update Tenant'}</span>
@@ -853,26 +872,6 @@ export const TenantManagementPage: React.FC = () => {
<p className="text-sm font-bold text-white mt-0.5">#{selectedTenant.id}</p>
</div>
<div>
<p className="text-[11px] text-slate-500 uppercase tracking-wider">Partition HashID</p>
<div className="flex items-center gap-2 mt-0.5">
<span className="font-mono text-xs text-blue-400 bg-blue-950/60 px-2 py-0.5 rounded border border-blue-800/50">
{encodeHashIdSync(selectedTenant.id)}
</span>
<button
onClick={() => handleCopyHashId(encodeHashIdSync(selectedTenant.id), `detail-${selectedTenant.id}`)}
className="text-slate-400 hover:text-white transition cursor-pointer"
title="Copy HashID"
>
{copiedId === `detail-${selectedTenant.id}` ? (
<Check className="w-3.5 h-3.5 text-emerald-400" />
) : (
<Copy className="w-3.5 h-3.5" />
)}
</button>
</div>
</div>
<div>
<p className="text-[11px] text-slate-500 uppercase tracking-wider">Tenant Name</p>
<p className="text-sm font-semibold text-slate-200 mt-0.5">{selectedTenant.name || '—'}</p>
@@ -892,7 +891,9 @@ export const TenantManagementPage: React.FC = () => {
<div>
<p className="text-[11px] text-slate-500 uppercase tracking-wider">Created By</p>
<p className="text-xs font-mono text-slate-300 mt-0.5">{selectedTenant.createdBy || 'System Admin'}</p>
<p className="text-xs font-mono text-slate-300 mt-0.5">
{getUserEmail(selectedTenant.createdBy) || selectedTenant.createdBy || 'System Admin'}
</p>
</div>
{selectedTenant.updatedAt && (
@@ -905,7 +906,9 @@ export const TenantManagementPage: React.FC = () => {
{selectedTenant.updatedBy && (
<div>
<p className="text-[11px] text-slate-500 uppercase tracking-wider">Updated By</p>
<p className="text-xs font-mono text-slate-300 mt-0.5">{selectedTenant.updatedBy}</p>
<p className="text-xs font-mono text-slate-300 mt-0.5">
{getUserEmail(selectedTenant.updatedBy) || selectedTenant.updatedBy}
</p>
</div>
)}
</div>
@@ -913,7 +916,7 @@ export const TenantManagementPage: React.FC = () => {
<div className="p-3.5 rounded-xl bg-slate-900/80 border border-slate-800 text-xs text-slate-300 space-y-2">
<p className="font-semibold text-white flex items-center gap-1.5">
<ShieldCheck className="w-4 h-4 text-blue-400" />
Partition Scope & Authentication
Partition Scope &amp; Authentication
</p>
<p className="text-[11px] text-slate-400 leading-relaxed">
When users authenticate, their security token contains claims identifying their tenant partition.
@@ -928,7 +931,7 @@ export const TenantManagementPage: React.FC = () => {
setIsDetailModalOpen(false);
handleOpenUpdate(selectedTenant);
}}
className="flex items-center gap-1.5 px-4 py-2 bg-blue-600 hover:bg-blue-500 text-white text-xs font-semibold rounded-lg transition cursor-pointer"
className="flex items-center gap-1.5 px-4 py-2 bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-500 hover:to-indigo-500 text-white text-xs font-semibold rounded-xl transition cursor-pointer"
>
<Edit2 className="w-3.5 h-3.5" />
<span>Edit Tenant Status</span>
@@ -936,7 +939,7 @@ export const TenantManagementPage: React.FC = () => {
<button
type="button"
onClick={() => setIsDetailModalOpen(false)}
className="px-4 py-2 bg-slate-800 hover:bg-slate-700 text-slate-300 text-xs font-medium rounded-lg transition cursor-pointer"
className="px-4 py-2 bg-slate-800 hover:bg-slate-700 text-slate-300 text-xs font-medium rounded-xl transition cursor-pointer"
>
Close
</button>
@@ -64,6 +64,7 @@ import {
ProblemDetails,
ActivityStatus,
Tenant,
TenantOrganisation,
CreateTenantRequest,
UpdateTenantRequest,
Beneficiary,
@@ -180,6 +181,7 @@ export const normalizeOrganisation = (raw: any): Organisation => {
name: raw.name ?? raw.Name ?? null,
registrationNo: raw.registrationNo ?? raw.RegistrationNo ?? raw.regNo ?? raw.RegNo ?? null,
email: raw.email ?? raw.Email ?? null,
userId: raw.userId ?? raw.UserId ?? raw.organisationUser ?? raw.OrganisationUser ?? null,
type: raw.type ?? raw.Type ?? raw.organisationType ?? raw.OrganisationType ?? 5,
status,
createdAt: raw.createdAt ?? raw.CreatedAt ?? new Date().toISOString(),
@@ -214,6 +216,8 @@ export const createOrganisation = async (payload: CreateOrganisationRequest): Pr
name: payload.name,
registrationNo: payload.registrationNo,
email: payload.email,
idNumber: payload.idNumber,
organisationUser: payload.organisationUser,
type: payload.type !== undefined ? Number(payload.type) : 5,
});
return res.data;
@@ -225,6 +229,8 @@ export const updateOrganisation = async (payload: UpdateOrganisationRequest): Pr
name: payload.name,
registrationNo: payload.registrationNo,
email: payload.email,
idNumber: payload.idNumber,
organisationUser: payload.organisationUser,
type: Number(payload.type),
status: payload.status !== undefined ? Number(payload.status) : ActivityStatus.Active,
});
@@ -1527,6 +1533,26 @@ export const updateTenant = async (payload: UpdateTenantRequest): Promise<void>
});
};
export const getTenantOrganisations = async (tenantId: string | number): Promise<TenantOrganisation[]> => {
const hashedTenantId = await toolsService.encodeId(tenantId);
try {
const res = await apiClient.get<TenantOrganisation[]>(
`/v1/api/governance/tenants/${encodeURIComponent(hashedTenantId)}/organisations`
);
return res.data || [];
} catch (err: any) {
if (String(tenantId) !== String(hashedTenantId)) {
try {
const fallbackRes = await apiClient.get<TenantOrganisation[]>(
`/v1/api/governance/tenants/${encodeURIComponent(String(tenantId))}/organisations`
);
return fallbackRes.data || [];
} catch {}
}
throw err;
}
};
// ==========================================
// 10. Projects: Beneficiaries & Outcomes APIs
// ==========================================
+18
View File
@@ -262,6 +262,7 @@ export interface User {
status: ActivityStatus;
lastLoginAt: string | null;
role?: string | UserRole | null;
idNumber?: string | null;
name?: string;
organisationId?: number | string;
organisationName?: string;
@@ -302,6 +303,7 @@ export interface Organisation {
name: string | null;
registrationNo: string | null;
email: string | null;
userId?: string | null;
type: OrganisationType;
status: ActivityStatus;
createdAt: string;
@@ -312,6 +314,8 @@ export interface CreateOrganisationRequest {
name: string;
registrationNo: string;
email: string;
idNumber: string;
organisationUser: string;
type: OrganisationType;
}
@@ -320,10 +324,24 @@ export interface UpdateOrganisationRequest {
name: string;
registrationNo: string;
email: string;
idNumber: string;
organisationUser: string;
type: OrganisationType;
status?: ActivityStatus;
}
export interface TenantOrganisation {
id: number | string;
tenantId: number | string;
organisationId: number | string;
status: ActivityStatus;
createdAt: string;
createdBy: string;
tenant?: Tenant | null;
organisation?: Organisation | null;
creator?: User | null;
}
export interface Portfolio {
id: number | string;
tenantId?: number | string;