Removed duplicate filter in settings page
Improved Rule value handling
This commit is contained in:
@@ -582,14 +582,15 @@ export const USER_ROLE_OPTIONS = [
|
|||||||
{ value: 'Supervisor', label: 'Supervisor' },
|
{ value: 'Supervisor', label: 'Supervisor' },
|
||||||
{ value: 'Manager', label: 'Manager' },
|
{ value: 'Manager', label: 'Manager' },
|
||||||
{ value: 'Contributor', label: 'Contributor' },
|
{ value: 'Contributor', label: 'Contributor' },
|
||||||
|
{ value: 'Unassigned', label: 'Unassigned' },
|
||||||
];
|
];
|
||||||
|
|
||||||
export const getUserRoleBadge = (role?: string | number | UserRole | null) => {
|
export const getUserRoleBadge = (role?: string | number | UserRole | null) => {
|
||||||
if (role === null || role === undefined || role === '') {
|
if (role === null || role === undefined || role === '' || String(role).trim().toLowerCase() === 'null' || String(role).trim().toLowerCase() === 'unassigned') {
|
||||||
return {
|
return {
|
||||||
label: 'Contributor',
|
label: 'Unassigned',
|
||||||
className: 'bg-emerald-500/15 text-emerald-300 border-emerald-500/30',
|
className: 'bg-slate-800/80 text-slate-400 border-slate-700/80 shadow-xs',
|
||||||
dot: 'bg-emerald-400',
|
dot: 'bg-slate-500',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -47,8 +47,6 @@ import {
|
|||||||
getUserSyncRecord,
|
getUserSyncRecord,
|
||||||
getInvites,
|
getInvites,
|
||||||
sendInvite,
|
sendInvite,
|
||||||
getApplicantInvite,
|
|
||||||
getApplicantInvites,
|
|
||||||
getOrganisations,
|
getOrganisations,
|
||||||
getAwards,
|
getAwards,
|
||||||
getContracts,
|
getContracts,
|
||||||
@@ -131,10 +129,11 @@ export const SettingsPage: React.FC = () => {
|
|||||||
|
|
||||||
const filteredUsers = useMemo(() => {
|
const filteredUsers = useMemo(() => {
|
||||||
return usersList.filter((u) => {
|
return usersList.filter((u) => {
|
||||||
|
const roleLabel = getUserRoleBadge(u.role).label;
|
||||||
const matchRole =
|
const matchRole =
|
||||||
userRoleFilter === 'all' ||
|
userRoleFilter === 'all' ||
|
||||||
(u.role && String(u.role).toLowerCase() === userRoleFilter.toLowerCase()) ||
|
roleLabel.toLowerCase() === userRoleFilter.toLowerCase() ||
|
||||||
(!u.role && userRoleFilter.toLowerCase() === 'contributor');
|
(u.role && String(u.role).toLowerCase() === userRoleFilter.toLowerCase());
|
||||||
return matchRole;
|
return matchRole;
|
||||||
});
|
});
|
||||||
}, [usersList, userRoleFilter]);
|
}, [usersList, userRoleFilter]);
|
||||||
@@ -198,9 +197,6 @@ export const SettingsPage: React.FC = () => {
|
|||||||
// 2. Communications & Invites State
|
// 2. Communications & Invites State
|
||||||
// ==========================================
|
// ==========================================
|
||||||
const [isSendInviteModalOpen, setIsSendInviteModalOpen] = useState(false);
|
const [isSendInviteModalOpen, setIsSendInviteModalOpen] = useState(false);
|
||||||
const [applicantSearchId, setApplicantSearchId] = useState('');
|
|
||||||
const [applicantInvitesResults, setApplicantInvitesResults] = useState<Invite[] | null>(null);
|
|
||||||
const [searchingApplicant, setSearchingApplicant] = useState(false);
|
|
||||||
|
|
||||||
// Invite Form State
|
// Invite Form State
|
||||||
const [inviteOrgId, setInviteOrgId] = useState<string | number>('');
|
const [inviteOrgId, setInviteOrgId] = useState<string | number>('');
|
||||||
@@ -266,37 +262,6 @@ export const SettingsPage: React.FC = () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleSearchApplicantInvites = async (e?: React.FormEvent) => {
|
|
||||||
if (e) e.preventDefault();
|
|
||||||
if (!applicantSearchId.trim()) return;
|
|
||||||
try {
|
|
||||||
setSearchingApplicant(true);
|
|
||||||
const res = await getApplicantInvites(applicantSearchId.trim());
|
|
||||||
setApplicantInvitesResults(res);
|
|
||||||
success('Lookup Complete', `Retrieved ${res.length} invitation records for applicant.`);
|
|
||||||
} catch (err: any) {
|
|
||||||
toastError('Lookup Error', err);
|
|
||||||
setApplicantInvitesResults([]);
|
|
||||||
} finally {
|
|
||||||
setSearchingApplicant(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleLookupSingleApplicantInvite = async () => {
|
|
||||||
if (!applicantSearchId.trim()) return;
|
|
||||||
try {
|
|
||||||
setSearchingApplicant(true);
|
|
||||||
const res = await getApplicantInvite(applicantSearchId.trim());
|
|
||||||
setApplicantInvitesResults(res ? [res] : []);
|
|
||||||
success('Primary Invite Found', `Retrieved primary invitation for applicant.`);
|
|
||||||
} catch (err: any) {
|
|
||||||
toastError('Lookup Error', err);
|
|
||||||
setApplicantInvitesResults([]);
|
|
||||||
} finally {
|
|
||||||
setSearchingApplicant(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// 3. Email Gateway & Notifications State
|
// 3. Email Gateway & Notifications State
|
||||||
// ==========================================
|
// ==========================================
|
||||||
@@ -908,71 +873,7 @@ export const SettingsPage: React.FC = () => {
|
|||||||
{/* TAB 2: Communications & Invites */}
|
{/* TAB 2: Communications & Invites */}
|
||||||
{/* ============================================================== */}
|
{/* ============================================================== */}
|
||||||
{activeTab === 'invites' && (
|
{activeTab === 'invites' && (
|
||||||
<div className="space-y-6">
|
<div className="space-y-4">
|
||||||
{/* Applicant Invites Lookup */}
|
|
||||||
<div className="bg-[#0f1523]/80 backdrop-blur-md p-5 rounded-2xl border border-slate-800/80 shadow-xl">
|
|
||||||
<h3 className="text-sm font-bold text-white mb-1">Applicant Invitations Lookup</h3>
|
|
||||||
<p className="text-xs text-slate-400 mb-3">
|
|
||||||
Query invitations dispatched to a specific candidate or grantee using their unique Applicant User UUID.
|
|
||||||
</p>
|
|
||||||
<form onSubmit={handleSearchApplicantInvites} className="flex gap-2">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={applicantSearchId}
|
|
||||||
onChange={(e) => setApplicantSearchId(e.target.value)}
|
|
||||||
placeholder="Enter Applicant User ID (UUID)..."
|
|
||||||
className="flex-1 px-3 py-2 text-xs bg-slate-900/80 border border-slate-700/80 rounded-xl text-white placeholder-slate-500 font-mono focus:outline-none focus:ring-2 focus:ring-blue-500/50"
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
disabled={searchingApplicant || !applicantSearchId.trim()}
|
|
||||||
className="px-3.5 py-2 text-xs font-semibold text-white bg-blue-600 hover:bg-blue-500 rounded-xl transition cursor-pointer disabled:opacity-50"
|
|
||||||
title="Lookup all invites"
|
|
||||||
>
|
|
||||||
{searchingApplicant ? 'Searching...' : 'All Invites'}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={handleLookupSingleApplicantInvite}
|
|
||||||
disabled={searchingApplicant || !applicantSearchId.trim()}
|
|
||||||
className="px-3.5 py-2 text-xs font-semibold text-blue-300 bg-blue-500/10 hover:bg-blue-500/20 border border-blue-500/30 rounded-xl transition cursor-pointer disabled:opacity-50"
|
|
||||||
title="Lookup primary invite"
|
|
||||||
>
|
|
||||||
Primary Invite
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
{applicantInvitesResults && (
|
|
||||||
<div className="mt-4 pt-4 border-t border-slate-800/80">
|
|
||||||
<span className="text-xs font-semibold text-slate-300">
|
|
||||||
Lookup Results ({applicantInvitesResults.length} matches):
|
|
||||||
</span>
|
|
||||||
{applicantInvitesResults.length === 0 ? (
|
|
||||||
<p className="text-xs text-slate-500 mt-2">No invitations found for this applicant UUID.</p>
|
|
||||||
) : (
|
|
||||||
<div className="mt-2 space-y-2">
|
|
||||||
{applicantInvitesResults.map((inv, idx) => (
|
|
||||||
<div
|
|
||||||
key={idx}
|
|
||||||
className="p-3 bg-slate-900/60 rounded-xl border border-slate-800 text-xs flex flex-col sm:flex-row sm:items-center justify-between gap-2"
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<span className="font-semibold text-white">{inv.message}</span>
|
|
||||||
<div className="text-[11px] text-slate-400 mt-0.5">
|
|
||||||
Org #{inv.organisationId} • Award #{inv.awardId} • Contract #{inv.contractId}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<span className="text-[10px] font-mono px-2 py-0.5 rounded bg-blue-500/10 text-blue-400 border border-blue-500/20">
|
|
||||||
{getNotificationPlatformLabel(inv.platform)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Standardized Toolbar & Filter Bar directly above Sent Invitations Table */}
|
{/* Standardized Toolbar & Filter Bar directly above Sent Invitations Table */}
|
||||||
<div className="bg-[#0f1523]/80 backdrop-blur-md p-3.5 sm:p-4 rounded-2xl border border-slate-800/80 shadow-xl flex flex-col lg:flex-row items-stretch lg:items-center justify-between gap-3">
|
<div className="bg-[#0f1523]/80 backdrop-blur-md p-3.5 sm:p-4 rounded-2xl border border-slate-800/80 shadow-xl flex flex-col lg:flex-row items-stretch lg:items-center justify-between gap-3">
|
||||||
{/* Filters on the left */}
|
{/* Filters on the left */}
|
||||||
|
|||||||
Reference in New Issue
Block a user