Refactored UI
Added Role filed to user table and endpoint implementation
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user