diff --git a/PostFundManagement.ConsoleUI/src/pages/GovernancePage.tsx b/PostFundManagement.ConsoleUI/src/pages/GovernancePage.tsx
index ea18858..23435d3 100644
--- a/PostFundManagement.ConsoleUI/src/pages/GovernancePage.tsx
+++ b/PostFundManagement.ConsoleUI/src/pages/GovernancePage.tsx
@@ -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 = () => {
+
+
+
+
+ Linked User Account *
+
+
+
+
{
+ 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) => (
+
+ {u.email || u.id} {u.role ? `(${u.role})` : ''} {u.idNumber ? `• ID: ${u.idNumber}` : ''}
+
+ ))}
+
+ {orgForm.formState.errors.organisationUser && (
+
{orgForm.formState.errors.organisationUser.message}
+ )}
+
+
+
+
+
+ Government Issued ID Number *
+
+
+
+
+ {orgForm.formState.errors.idNumber && (
+
{orgForm.formState.errors.idNumber.message}
+ )}
+
+
+
@@ -2103,6 +2183,12 @@ export const GovernancePage: React.FC = () => {
Entity Type:
{getOrganisationTypeLabel(orgFreshDetail?.type ?? selectedOrgForDetail?.type ?? OrganisationType.Enterprise)}
+
+ Linked User:
+
+ {getUserEmail(orgFreshDetail?.userId || selectedOrgForDetail?.userId) || orgFreshDetail?.userId || selectedOrgForDetail?.userId || '—'}
+
+
diff --git a/PostFundManagement.ConsoleUI/src/pages/SettingsPage.tsx b/PostFundManagement.ConsoleUI/src/pages/SettingsPage.tsx
index 0d2a8ee..f831952 100644
--- a/PostFundManagement.ConsoleUI/src/pages/SettingsPage.tsx
+++ b/PostFundManagement.ConsoleUI/src/pages/SettingsPage.tsx
@@ -441,6 +441,7 @@ export const SettingsPage: React.FC = () => {
const userExportColumns: ExportColumn[] = [
{ 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 = () => {
User Identifier
+
+ Id Number
+
Role
@@ -785,13 +789,13 @@ export const SettingsPage: React.FC = () => {
{loadingUsers ? (
-
+
Loading user directory...
) : sortedUsers.length === 0 ? (
-
+
No users found matching current filters.
@@ -806,6 +810,9 @@ export const SettingsPage: React.FC = () => {
{u.id}
+
+ {u.idNumber || — }
+
@@ -2113,6 +2120,12 @@ export const SettingsPage: React.FC = () => {
{userFreshDetail?.id || selectedUserForDetail.id}
+
+ Id Number:
+
+ {userFreshDetail?.idNumber || selectedUserForDetail.idNumber || '—'}
+
+
System Role:
diff --git a/PostFundManagement.ConsoleUI/src/pages/TenantManagementPage.tsx b/PostFundManagement.ConsoleUI/src/pages/TenantManagementPage.tsx
index 8d7afac..23fded3 100644
--- a/PostFundManagement.ConsoleUI/src/pages/TenantManagementPage.tsx
+++ b/PostFundManagement.ConsoleUI/src/pages/TenantManagementPage.tsx
@@ -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
;
type UpdateTenantFormValues = z.infer;
+// 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({
+ queryKey: ['tenant-organisations', tenantId],
+ queryFn: () => getTenantOrganisations(tenantId),
+ staleTime: 30000,
+ });
+
+ return (
+
+ {/* Sub-list Header */}
+
+
+
+
+
+
+
+
+ Scoped Organisations for {tenantName || `Tenant #${tenantId}`}
+
+
+ {tenantOrganisations.length} Legal {tenantOrganisations.length === 1 ? 'Entity' : 'Entities'}
+
+
+
+ Legal entities and holding structures mapped under Tenant ID #{tenantId} partition boundary (Read-Only).
+
+
+
+
+
+
refetch()}
+ className="p-1 text-slate-400 hover:text-white rounded hover:bg-slate-800 transition cursor-pointer"
+ title="Refresh organisations list"
+ >
+
+
+
+ Organisation management is handled in Governance & Structure
+
+
+
+
+ {/* Organisations Sub-Table */}
+ {isLoading ? (
+
+
+ Loading partition organisations from server...
+
+ ) : isError ? (
+
+
Unable to retrieve organisations for Tenant #{tenantId}.
+
refetch()}
+ className="mt-1 text-[11px] text-blue-400 hover:underline cursor-pointer"
+ >
+ Retry Query
+
+
+ ) : tenantOrganisations.length === 0 ? (
+
+
+
No organisations assigned to Tenant #{tenantId}
+
+ New legal entities provisioned with Tenant ID #{tenantId} will automatically appear in this partition view.
+
+
+ ) : (
+
+
+
+
+ Org ID
+ Organisation Name
+ Registration No.
+ Entity Type
+ Contact Email
+ Linked User Email
+ Status
+ Created Date
+
+
+
+ {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 (
+
+ {/* Integer Org ID only (no hashid) */}
+
+ #{orgId}
+
+
+ {/* Organisation Name */}
+
+ {orgName}
+
+
+ {/* Registration No */}
+
+ {regNo}
+
+
+ {/* Entity Type */}
+
+
+ {orgType !== undefined ? getOrganisationTypeLabel(orgType) : 'Organisation'}
+
+
+
+ {/* Contact Email */}
+
+ {orgEmail && orgEmail !== '—' ? (
+
+
+ {orgEmail}
+
+ ) : (
+ —
+ )}
+
+
+ {/* Linked User (swapped raw ID for email) */}
+
+
+
+
+ {userEmailDisplay}
+
+
+
+
+ {/* Status */}
+
+
+
+
+ {/* Created Date */}
+
+ {formatDate(createdAt)}
+
+
+ );
+ })}
+
+
+
+ )}
+
+ );
+};
+
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('all');
- const [copiedId, setCopiedId] = useState(null);
const [expandedTenants, setExpandedTenants] = useState>({});
// 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({
- queryKey: ['organisations'],
- queryFn: () => getOrganisations(),
- staleTime: 60000,
- });
-
// Forms
const createForm = useForm({
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[] = [
{ 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 (
-
- {/* Top Header */}
-
-
-
-
-
+
+ {/* Header Banner - Conforming to standard page design */}
+
+
+
+
+
+
+
+
+
+
+ Tenant Management
+
+
+ Directory
+
+
+
+
+ Manage multi-tenant isolated partitions, statutory boundaries, and enterprise organizational tenancy.
+
-
Tenant Management
-
- Multi-Tenancy
-
-
- 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.
-
-
+
-
+
Refresh
New Tenant
@@ -272,7 +435,7 @@ export const TenantManagementPage: React.FC = () => {
{/* Summary KPI Cards */}
{/* Total Tenants */}
-
+
Total Partitions
{loadingTenants ? '—' : totalCount}
@@ -284,7 +447,7 @@ export const TenantManagementPage: React.FC = () => {
{/* Active Tenants */}
-
+
Active Tenants
{loadingTenants ? '—' : activeCount}
@@ -296,7 +459,7 @@ export const TenantManagementPage: React.FC = () => {
{/* Inactive / Disabled Tenants */}
-
+
Locked / Inactive
{loadingTenants ? '—' : inactiveCount + disabledCount}
@@ -308,7 +471,7 @@ export const TenantManagementPage: React.FC = () => {
{/* Current Active Tenant */}
-
+
Your Active Partition
@@ -321,8 +484,8 @@ export const TenantManagementPage: React.FC = () => {
{currentTenantName || `Tenant #${currentTenantId || 1}`}
-
- HashID: {currentHashedTenantId || '—'}
+
+ Tenant ID: #{currentTenantId || 1}
@@ -332,23 +495,23 @@ export const TenantManagementPage: React.FC = () => {
{/* Search & Filter Bar */}
-
+
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"
/>
-
+
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 = () => {
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 = () => {
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 = () => {
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 = () => {
{/* Tenants Table */}
-
+
-
+
Expand
- Tenant ID & HashID
+ Tenant ID
Tenant Organization Name
@@ -441,19 +604,11 @@ export const TenantManagementPage: React.FC = () => {
) : (
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 (
@@ -477,28 +632,12 @@ export const TenantManagementPage: React.FC = () => {
- {/* ID & HashID */}
-
-
- #{tenant.id}
-
- {hashId}
-
- handleCopyHashId(hashId, String(tenant.id))}
- className="text-slate-500 hover:text-slate-300 transition cursor-pointer"
- title="Copy HashID"
- >
- {copiedId === String(tenant.id) ? (
-
- ) : (
-
- )}
-
-
+ {/* Integer Tenant ID only (no hashid) */}
+
+ #{tenant.id}
- {/* Tenant Name & Sub-Organisations Count */}
+ {/* Tenant Name */}
@@ -508,13 +647,6 @@ export const TenantManagementPage: React.FC = () => {
Active Context
)}
- 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"
- >
- {tenantOrganisations.length} Org{tenantOrganisations.length === 1 ? '' : 's'}
-
@@ -534,9 +666,11 @@ export const TenantManagementPage: React.FC = () => {
- {/* Created By */}
+ {/* Created By (Swapped ID for Email where available) */}
- {tenant.createdBy || 'System Administrator'}
+
+ {creatorEmail}
+
{/* Created Date */}
@@ -566,126 +700,11 @@ export const TenantManagementPage: React.FC = () => {
- {/* Expanded Sub-List of Organisations */}
+ {/* Expanded Sub-List of Organisations using getTenantOrganisations */}
{isExpanded && (
-
- {/* Sub-list Header */}
-
-
-
-
-
-
-
-
- Scoped Organisations for {tenant.name || `Tenant #${tenant.id}`}
-
-
- {tenantOrganisations.length} Legal {tenantOrganisations.length === 1 ? 'Entity' : 'Entities'}
-
-
-
- Legal entities and holding structures mapped under Tenant ID #{tenant.id} partition boundary (Read-Only).
-
-
-
-
-
- Organisation management is handled in Governance & Structure
-
-
-
- {/* Organisations Sub-Table */}
- {loadingOrgs ? (
-
-
- Loading organisations...
-
- ) : tenantOrganisations.length === 0 ? (
-
-
-
No organisations assigned to this tenant
-
- New organisations provisioned with Tenant ID #{tenant.id} will automatically appear in this partition view.
-
-
- ) : (
-
-
-
-
- Org ID & HashID
- Organisation Name
- Registration No.
- Entity Type
- Official Contact
- Status
- Created Date
-
-
-
- {tenantOrganisations.map((org) => {
- const orgHashId = encodeHashIdSync(org.id);
- return (
-
- {/* ID & HashID */}
-
-
- #{org.id}
-
- {orgHashId}
-
-
-
-
- {/* Name */}
-
- {org.name || 'Unnamed Organisation'}
-
-
- {/* Registration No */}
-
- {org.registrationNo || '—'}
-
-
- {/* Type */}
-
-
- {getOrganisationTypeLabel(org.type)}
-
-
-
- {/* Email */}
-
- {org.email ? (
-
-
- {org.email}
-
- ) : (
- —
- )}
-
-
- {/* Status */}
-
-
-
-
- {/* Created Date */}
-
- {formatDate(org.createdAt)}
-
-
- );
- })}
-
-
-
- )}
-
+
)}
@@ -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 && (
{createForm.formState.errors.name.message}
@@ -741,14 +760,14 @@ export const TenantManagementPage: React.FC = () => {
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
{createMutation.isPending && }
{createMutation.isPending ? 'Provisioning...' : 'Create Tenant'}
@@ -782,7 +801,7 @@ export const TenantManagementPage: React.FC = () => {
{updateForm.formState.errors.name && (
{updateForm.formState.errors.name.message}
@@ -795,7 +814,7 @@ export const TenantManagementPage: React.FC = () => {
Active (Users Permitted)
Inactive (Tenant Lockout)
@@ -821,14 +840,14 @@ export const TenantManagementPage: React.FC = () => {
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
{updateMutation.isPending && }
{updateMutation.isPending ? 'Saving...' : 'Update Tenant'}
@@ -853,26 +872,6 @@ export const TenantManagementPage: React.FC = () => {
#{selectedTenant.id}
-
-
Partition HashID
-
-
- {encodeHashIdSync(selectedTenant.id)}
-
- handleCopyHashId(encodeHashIdSync(selectedTenant.id), `detail-${selectedTenant.id}`)}
- className="text-slate-400 hover:text-white transition cursor-pointer"
- title="Copy HashID"
- >
- {copiedId === `detail-${selectedTenant.id}` ? (
-
- ) : (
-
- )}
-
-
-
-
Tenant Name
{selectedTenant.name || '—'}
@@ -892,7 +891,9 @@ export const TenantManagementPage: React.FC = () => {
Created By
-
{selectedTenant.createdBy || 'System Admin'}
+
+ {getUserEmail(selectedTenant.createdBy) || selectedTenant.createdBy || 'System Admin'}
+
{selectedTenant.updatedAt && (
@@ -905,7 +906,9 @@ export const TenantManagementPage: React.FC = () => {
{selectedTenant.updatedBy && (
Updated By
-
{selectedTenant.updatedBy}
+
+ {getUserEmail(selectedTenant.updatedBy) || selectedTenant.updatedBy}
+
)}
@@ -913,7 +916,7 @@ export const TenantManagementPage: React.FC = () => {
- Partition Scope & Authentication
+ Partition Scope & Authentication
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"
>
Edit Tenant Status
@@ -936,7 +939,7 @@ export const TenantManagementPage: React.FC = () => {
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
diff --git a/PostFundManagement.ConsoleUI/src/services/api.ts b/PostFundManagement.ConsoleUI/src/services/api.ts
index 8497e17..9acc636 100644
--- a/PostFundManagement.ConsoleUI/src/services/api.ts
+++ b/PostFundManagement.ConsoleUI/src/services/api.ts
@@ -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
});
};
+export const getTenantOrganisations = async (tenantId: string | number): Promise => {
+ const hashedTenantId = await toolsService.encodeId(tenantId);
+ try {
+ const res = await apiClient.get(
+ `/v1/api/governance/tenants/${encodeURIComponent(hashedTenantId)}/organisations`
+ );
+ return res.data || [];
+ } catch (err: any) {
+ if (String(tenantId) !== String(hashedTenantId)) {
+ try {
+ const fallbackRes = await apiClient.get(
+ `/v1/api/governance/tenants/${encodeURIComponent(String(tenantId))}/organisations`
+ );
+ return fallbackRes.data || [];
+ } catch {}
+ }
+ throw err;
+ }
+};
+
// ==========================================
// 10. Projects: Beneficiaries & Outcomes APIs
// ==========================================
diff --git a/PostFundManagement.ConsoleUI/src/types.ts b/PostFundManagement.ConsoleUI/src/types.ts
index b8e8f0d..a4336aa 100644
--- a/PostFundManagement.ConsoleUI/src/types.ts
+++ b/PostFundManagement.ConsoleUI/src/types.ts
@@ -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;