Refactored UI

Added Role filed to user table and endpoint implementation
This commit is contained in:
2026-09-28 10:25:00 +02:00
parent 7037ea72ca
commit ea71e55e2d
21 changed files with 3100 additions and 128 deletions
@@ -83,6 +83,7 @@ export const BeneficiariesPage: React.FC = () => {
const [isCompModalOpen, setIsCompModalOpen] = useState(false);
const [editingComp, setEditingComp] = useState<ComplianceItem | null>(null);
const [compTargetProjectId, setCompTargetProjectId] = useState<string>('');
const [selectedBenForDetail, setSelectedBenForDetail] = useState<Beneficiary | null>(null);
const [isDetailModalOpen, setIsDetailModalOpen] = useState(false);
@@ -109,16 +110,44 @@ export const BeneficiariesPage: React.FC = () => {
enabled: !!effectiveProjectId,
});
// Query compliance items
// Refactored compliance items data retrieval:
// Fetches from api/projects/{projectId}/complianceitems for the selected project,
// or across all projects in the portfolio when 'all' is chosen.
const {
data: complianceItems = [],
isLoading: loadingComps,
isError: isCompsError,
refetch: refetchComps,
isFetching: fetchingComps,
} = useQuery({
queryKey: ['complianceItems', effectiveProjectId],
queryFn: () => (effectiveProjectId ? getProjectComplianceItems(effectiveProjectId) : Promise.resolve([])),
enabled: !!effectiveProjectId,
queryKey: ['complianceItems', selectedProjectId, projects.map((p) => p.id).join(',')],
queryFn: async (): Promise<ComplianceItem[]> => {
if (selectedProjectId !== 'all') {
const items = await getProjectComplianceItems(selectedProjectId);
return (items || []).map((ci) => ({
...ci,
projectId: ci.projectId || selectedProjectId,
}));
}
if (!projects || projects.length === 0) return [];
const settled = await Promise.allSettled(
projects.map(async (p) => {
const items = await getProjectComplianceItems(p.id);
return (items || []).map((ci) => ({
...ci,
projectId: ci.projectId || p.id,
}));
})
);
const combined: ComplianceItem[] = [];
settled.forEach((res) => {
if (res.status === 'fulfilled' && Array.isArray(res.value)) {
combined.push(...res.value);
}
});
return combined;
},
enabled: projects.length > 0,
});
// Query outcomes for selected beneficiary
@@ -190,8 +219,9 @@ export const BeneficiariesPage: React.FC = () => {
const createCompMutation = useMutation({
mutationFn: (vals: ComplianceFormValues) => {
if (!effectiveProjectId) throw new Error('No project selected');
return createComplianceItem({ projectId: effectiveProjectId, title: vals.title, requirements: vals.requirements });
const targetProjId = compTargetProjectId || (selectedProjectId !== 'all' ? selectedProjectId : projects[0]?.id);
if (!targetProjId) throw new Error('No project selected');
return createComplianceItem({ projectId: targetProjId, title: vals.title, requirements: vals.requirements });
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['complianceItems'] });
@@ -285,6 +315,11 @@ export const BeneficiariesPage: React.FC = () => {
];
const compExportColumns: ExportColumn<ComplianceItem>[] = [
{
header: 'Project',
key: 'projectId',
format: (val) => projects.find((p) => String(p.id) === String(val))?.name || 'Project',
},
{ header: 'Compliance Requirement', key: 'title' },
{ header: 'Requirements Detail', key: 'requirements' },
{ header: 'Status', key: 'status', format: (val) => (val === ApprovalStatus.Approved ? 'Approved' : 'Pending Review') },
@@ -347,10 +382,18 @@ export const BeneficiariesPage: React.FC = () => {
<Users className="w-6 h-6" />
</div>
<div>
<h1 className="text-2xl sm:text-3xl font-bold tracking-tight text-white flex items-center gap-2.5">
Beneficiaries & Compliance Register
</h1>
<p className="text-xs sm:text-sm text-slate-400">
<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">
Beneficiaries & Compliance Register
</h1>
<HelpTooltip
title="Beneficiary & Compliance Framework"
content="Record target population demographics, track real-world participant reach vs targets, and monitor statutory compliance records across projects."
size="sm"
placement="bottom"
/>
</div>
<p className="text-xs sm:text-sm text-slate-400 mt-1">
Manage target recipient demographics, track reached counts, and record verified statutory compliance items.
</p>
</div>
@@ -362,7 +405,14 @@ export const BeneficiariesPage: React.FC = () => {
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
<div className="bg-[#0f1523]/80 backdrop-blur-md p-5 rounded-2xl border border-slate-800/80 shadow-xl space-y-2">
<div className="flex items-center justify-between text-slate-400">
<span className="text-xs font-semibold uppercase tracking-wider">Beneficiary Cohorts</span>
<div className="flex items-center gap-1.5">
<span className="text-xs font-semibold uppercase tracking-wider">Beneficiary Cohorts</span>
<HelpTooltip
title="Beneficiary Cohorts"
content="Registered demographic stakeholder groups receiving direct programmatic support."
size="sm"
/>
</div>
<Users className="w-4 h-4 text-blue-400" />
</div>
<p className="text-2xl font-bold text-white">{beneficiaries.length}</p>
@@ -371,7 +421,14 @@ export const BeneficiariesPage: React.FC = () => {
<div className="bg-[#0f1523]/80 backdrop-blur-md p-5 rounded-2xl border border-slate-800/80 shadow-xl space-y-2">
<div className="flex items-center justify-between text-slate-400">
<span className="text-xs font-semibold uppercase tracking-wider">Total Target Reach</span>
<div className="flex items-center gap-1.5">
<span className="text-xs font-semibold uppercase tracking-wider">Total Target Reach</span>
<HelpTooltip
title="Target Reach"
content="Total planned participant headcount agreed upon across project targets."
size="sm"
/>
</div>
<Target className="w-4 h-4 text-indigo-400" />
</div>
<p className="text-2xl font-bold text-white">{totalTarget.toLocaleString()}</p>
@@ -380,7 +437,14 @@ export const BeneficiariesPage: React.FC = () => {
<div className="bg-[#0f1523]/80 backdrop-blur-md p-5 rounded-2xl border border-slate-800/80 shadow-xl space-y-2">
<div className="flex items-center justify-between text-slate-400">
<span className="text-xs font-semibold uppercase tracking-wider">Actual Reached</span>
<div className="flex items-center gap-1.5">
<span className="text-xs font-semibold uppercase tracking-wider">Actual Reached</span>
<HelpTooltip
title="Actual Reached Headcount"
content="Cumulative verified individual or entity participants supported to date."
size="sm"
/>
</div>
<Award className="w-4 h-4 text-emerald-400" />
</div>
<div className="flex items-baseline gap-2">
@@ -394,7 +458,14 @@ export const BeneficiariesPage: React.FC = () => {
<div className="bg-[#0f1523]/80 backdrop-blur-md p-5 rounded-2xl border border-slate-800/80 shadow-xl space-y-2">
<div className="flex items-center justify-between text-slate-400">
<span className="text-xs font-semibold uppercase tracking-wider">Compliance Items</span>
<div className="flex items-center gap-1.5">
<span className="text-xs font-semibold uppercase tracking-wider">Compliance Items</span>
<HelpTooltip
title="Compliance Items"
content="Statutory, environmental, labor, and financial audit verification requirements."
size="sm"
/>
</div>
<ShieldCheck className="w-4 h-4 text-cyan-400" />
</div>
<div className="flex items-baseline gap-2">
@@ -476,6 +547,7 @@ export const BeneficiariesPage: React.FC = () => {
onChange={(e) => setSelectedProjectId(e.target.value)}
className="bg-slate-900 border border-slate-800 rounded-xl px-3 py-1.5 text-xs text-white focus:outline-none focus:border-blue-500 max-w-[180px] truncate"
>
<option value="all">All Projects</option>
{projects.map((p) => (
<option key={p.id} value={p.id}>
{p.name}
@@ -660,6 +732,7 @@ export const BeneficiariesPage: React.FC = () => {
onChange={(e) => setSelectedProjectId(e.target.value)}
className="bg-slate-900 border border-slate-800 rounded-xl px-3 py-1.5 text-xs text-white focus:outline-none focus:border-blue-500 max-w-[180px] truncate"
>
<option value="all">All Projects</option>
{projects.map((p) => (
<option key={p.id} value={p.id}>
{p.name}
@@ -695,6 +768,7 @@ export const BeneficiariesPage: React.FC = () => {
<button
onClick={() => {
setEditingComp(null);
setCompTargetProjectId(selectedProjectId !== 'all' ? selectedProjectId : projects[0]?.id ? String(projects[0].id) : '');
compForm.reset({ title: '', requirements: '', status: ApprovalStatus.Pending });
setIsCompModalOpen(true);
}}
@@ -714,6 +788,11 @@ export const BeneficiariesPage: React.FC = () => {
<SortableTh column="title" currentColumn={compSortCol} direction={compSortDir} onSort={handleCompSort} className="py-3.5 px-5 whitespace-nowrap">
Compliance Title
</SortableTh>
{selectedProjectId === 'all' && (
<SortableTh column="projectId" currentColumn={compSortCol} direction={compSortDir} onSort={handleCompSort} className="py-3.5 px-4 whitespace-nowrap">
Project
</SortableTh>
)}
<SortableTh column="requirements" currentColumn={compSortCol} direction={compSortDir} onSort={handleCompSort} className="py-3.5 px-4 whitespace-nowrap">
Requirements
</SortableTh>
@@ -727,9 +806,25 @@ export const BeneficiariesPage: React.FC = () => {
</tr>
</thead>
<tbody className="divide-y divide-slate-800/60">
{loadingComps ? (
{isCompsError ? (
<tr>
<td colSpan={5} className="py-12 text-center text-slate-400">
<td colSpan={selectedProjectId === 'all' ? 6 : 5} className="py-12 text-center text-rose-400">
<div className="flex flex-col items-center gap-2">
<AlertCircle className="w-6 h-6 text-rose-400" />
<span>Unable to load compliance items for this project from API.</span>
<button
type="button"
onClick={() => refetchComps()}
className="mt-2 px-3 py-1 bg-slate-800 text-white rounded-lg border border-slate-700 text-xs hover:bg-slate-700 transition cursor-pointer"
>
Retry
</button>
</div>
</td>
</tr>
) : loadingComps ? (
<tr>
<td colSpan={selectedProjectId === 'all' ? 6 : 5} className="py-12 text-center text-slate-400">
<div className="flex flex-col items-center gap-2">
<div className="w-6 h-6 border-2 border-blue-500 border-t-transparent rounded-full animate-spin" />
<span>Loading compliance items...</span>
@@ -738,8 +833,10 @@ export const BeneficiariesPage: React.FC = () => {
</tr>
) : sortedComps.length === 0 ? (
<tr>
<td colSpan={5} className="py-12 text-center text-slate-400">
No compliance items recorded for this project.
<td colSpan={selectedProjectId === 'all' ? 6 : 5} className="py-12 text-center text-slate-400">
{selectedProjectId === 'all'
? 'No compliance items recorded across all projects.'
: 'No compliance items recorded for this project.'}
</td>
</tr>
) : (
@@ -751,6 +848,13 @@ export const BeneficiariesPage: React.FC = () => {
<span>{c.title}</span>
</div>
</td>
{selectedProjectId === 'all' && (
<td className="py-3.5 px-4 whitespace-nowrap text-slate-300 font-medium">
<span className="px-2 py-0.5 rounded-md bg-slate-800/90 text-slate-200 border border-slate-700/60 text-[11px]">
{projects.find((p) => String(p.id) === String(c.projectId))?.name || 'Project'}
</span>
</td>
)}
<td className="py-3.5 px-4 text-slate-300 max-w-xs truncate" title={c.requirements || ''}>
{c.requirements || 'None'}
</td>
@@ -869,6 +973,23 @@ export const BeneficiariesPage: React.FC = () => {
onSubmit={compForm.handleSubmit((d) => (editingComp ? updateCompMutation.mutate(d) : createCompMutation.mutate(d)))}
className="space-y-4"
>
{!editingComp && (
<div>
<label className="block text-xs font-semibold text-slate-300 mb-1">Target Project *</label>
<select
value={compTargetProjectId || (selectedProjectId !== 'all' ? selectedProjectId : projects[0]?.id ? String(projects[0].id) : '')}
onChange={(e) => setCompTargetProjectId(e.target.value)}
className="w-full bg-slate-900 border border-slate-800 rounded-xl px-3 py-2 text-xs text-white focus:outline-none focus:border-blue-500"
>
{projects.map((p) => (
<option key={p.id} value={p.id}>
{p.name}
</option>
))}
</select>
</div>
)}
<div>
<label className="block text-xs font-semibold text-slate-300 mb-1">Compliance Title *</label>
<input