Create Organisation Fix

This commit is contained in:
2026-10-20 12:47:06 +02:00
parent 28a821ae91
commit d7a168c9fa
17 changed files with 527 additions and 155 deletions
@@ -82,7 +82,7 @@ export function ExportButton<T>({
columns,
data,
tenantName: (claims as any)?.tenant_name || (claims as any)?.tenant || `Tenant #${tenantId || 1}`,
userEmail: user?.profile?.email || (claims as any)?.email || '',
userEmail: (user as any)?.profile?.email || user?.email || (claims as any)?.email || '',
});
};
@@ -96,7 +96,7 @@ export function ExportButton<T>({
columns,
data,
tenantName: (claims as any)?.tenant_name || (claims as any)?.tenant || `Tenant #${tenantId || 1}`,
userEmail: user?.profile?.email || (claims as any)?.email || '',
userEmail: (user as any)?.profile?.email || user?.email || (claims as any)?.email || '',
});
};
@@ -2,14 +2,15 @@ import React, { ReactNode, useEffect } from 'react';
import { X } from 'lucide-react';
import { HelpTooltip } from './HelpTooltip';
interface ModalProps {
export interface ModalProps {
isOpen: boolean;
onClose: () => void;
title: string;
subtitle?: string;
helpText?: ReactNode;
children: ReactNode;
maxWidth?: 'sm' | 'md' | 'lg' | 'xl' | '2xl';
maxWidth?: 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl' | '4xl' | '5xl' | '6xl' | 'full';
size?: 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl' | '4xl' | '5xl' | '6xl' | 'full' | string;
}
export const Modal: React.FC<ModalProps> = ({
@@ -20,6 +21,7 @@ export const Modal: React.FC<ModalProps> = ({
helpText,
children,
maxWidth = 'lg',
size,
}) => {
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
@@ -43,13 +45,20 @@ export const Modal: React.FC<ModalProps> = ({
if (!isOpen) return null;
const maxWidthClass = {
const effectiveSize = (size || maxWidth) as string;
const maxWidthClassMap: Record<string, string> = {
sm: 'max-w-sm',
md: 'max-w-md',
lg: 'max-w-lg',
xl: 'max-w-xl',
'2xl': 'max-w-2xl',
}[maxWidth];
'3xl': 'max-w-3xl',
'4xl': 'max-w-4xl',
'5xl': 'max-w-5xl',
'6xl': 'max-w-6xl',
full: 'max-w-full',
};
const maxWidthClass = maxWidthClassMap[effectiveSize] || 'max-w-lg';
return (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/80 backdrop-blur-sm animate-in fade-in duration-150">
@@ -0,0 +1,82 @@
import React, { useState } from 'react';
import { Eye, EyeOff, Copy, Check } from 'lucide-react';
import { useToast } from './Toast';
export interface ObfuscatedIdNumberProps {
idNumber?: string | null;
className?: string;
allowToggle?: boolean;
}
export function maskIdNumber(id: string): string {
const clean = id ? id.trim() : '';
if (clean.length <= 4) {
return '••••';
}
if (clean.length <= 8) {
return `${clean.slice(0, 2)}••••${clean.slice(-2)}`;
}
// SA ID (13 digits) or standard national IDs: keep first 6 (DOB), mask middle 5, show last 2
return `${clean.slice(0, 6)}•••••${clean.slice(-2)}`;
}
export function ObfuscatedIdNumber({
idNumber,
className = '',
allowToggle = true,
}: ObfuscatedIdNumberProps): React.ReactElement {
const [isRevealed, setIsRevealed] = useState<boolean>(false);
const [copied, setCopied] = useState<boolean>(false);
const { success } = useToast();
if (!idNumber) {
return <span className="text-slate-600 font-mono text-xs">-</span>;
}
const rawId: string = idNumber;
const handleCopy = async (e: React.MouseEvent<HTMLButtonElement>): Promise<void> => {
e.stopPropagation();
try {
await navigator.clipboard.writeText(rawId);
setCopied(true);
success('Copied to Clipboard', 'ID Number copied.');
setTimeout(() => setCopied(false), 2000);
} catch {
// Clipboard fallback
}
};
const handleToggle = (e: React.MouseEvent<HTMLButtonElement>): void => {
e.stopPropagation();
setIsRevealed((prev) => !prev);
};
const displayedText = isRevealed ? rawId : maskIdNumber(rawId);
return (
<div className={`inline-flex items-center gap-1.5 font-mono text-xs text-slate-300 ${className}`}>
<span className="tracking-wider select-all">{displayedText}</span>
{allowToggle && (
<button
type="button"
onClick={handleToggle}
title={isRevealed ? 'Hide ID Number' : 'Reveal ID Number'}
className="p-1 rounded hover:bg-slate-800 text-slate-400 hover:text-slate-200 transition-colors cursor-pointer"
>
{isRevealed ? <EyeOff className="w-3.5 h-3.5 text-blue-400" /> : <Eye className="w-3.5 h-3.5" />}
</button>
)}
<button
type="button"
onClick={handleCopy}
title="Copy ID Number"
className="p-1 rounded hover:bg-slate-800 text-slate-400 hover:text-slate-200 transition-colors cursor-pointer"
>
{copied ? <Check className="w-3.5 h-3.5 text-emerald-400" /> : <Copy className="w-3.5 h-3.5" />}
</button>
</div>
);
}
@@ -6,7 +6,7 @@ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { UserPlus, Shield, Hash, Mail, Phone, Lock, Building, CheckCircle2 } from 'lucide-react';
import { Modal } from '../ui/Modal';
import { useToast } from '../ui/Toast';
import { createUser, getSystemRoles, assignUserRole, getTenants } from '../../services/api';
import { createUser, getSystemRoles, assignUserRole, getTenants, getUsers } from '../../services/api';
import { useAuth } from '../../auth/AuthProvider';
import { useRbac } from '../../auth/rbac';
import { CreateUserRequest, Role, Tenant } from '../../types';
@@ -136,8 +136,22 @@ export const CreateUserModal: React.FC<CreateUserModalProps> = ({
await createUser(values.tenantId, values.idNumber.trim(), payload);
// If an initial role was chosen and we have a newly generated user,
// invalidate query cache to refresh directory
if (values.initialRoleId) {
try {
await queryClient.invalidateQueries({ queryKey: ['users-directory'] });
await queryClient.invalidateQueries({ queryKey: ['users'] });
const freshUsers = await getUsers(1, 1000, { email: values.email.trim() });
const createdUser = freshUsers.find(
(u) => u.email && u.email.toLowerCase() === values.email.trim().toLowerCase()
);
if (createdUser) {
await assignUserRole(createdUser.id, values.initialRoleId);
}
} catch {
// Graceful fallback
}
}
return values;
},
onSuccess: (values) => {
@@ -32,6 +32,7 @@ import {
getSystemClaims,
} from '../../services/api';
import { HelpTooltip } from '../ui/HelpTooltip';
import { ObfuscatedIdNumber } from '../ui/ObfuscatedIdNumber';
import { decodeIdNumberHex } from '../../utils/hashids';
import { User, Role, Claim, ActivityStatus } from '../../types';
import { formatDate, getActivityStatusBadge, getUserRoleBadge } from '../../lib/formatters';
@@ -298,10 +299,14 @@ export const UserSecurityModal: React.FC<UserSecurityModalProps> = ({
<div className="grid grid-cols-2 gap-3 border-t border-slate-800/80 pt-2">
<div>
<span className="text-slate-400 font-semibold block text-[11px] uppercase">Id Number:</span>
<div className="flex items-center gap-2">
<span className="font-mono text-white text-xs">{effectiveUser.idNumber || '—'}</span>
<div className="flex items-center gap-2 mt-0.5">
<ObfuscatedIdNumber idNumber={effectiveUser.idNumber} />
{decodedId && decodedId !== effectiveUser.idNumber && (
<span className="text-[10px] text-blue-400 font-mono">(Decoded: {decodedId})</span>
<div className="flex items-center gap-1 text-[10px] text-blue-400 font-mono">
<span>(Decoded:</span>
<ObfuscatedIdNumber idNumber={decodedId} />
<span>)</span>
</div>
)}
</div>
</div>