Fixed UI misalignments

This commit is contained in:
2026-10-26 19:57:32 +02:00
parent 80c798e5e6
commit 9f132eeb29
22 changed files with 860 additions and 214 deletions
@@ -42,6 +42,7 @@ import {
recordIndicatorActual,
getBudgets,
getActivities,
getDisbursements,
} from '../services/api';
import { Modal } from '../components/ui/Modal';
import { HelpTooltip } from '../components/ui/HelpTooltip';
@@ -242,6 +243,11 @@ export const ProjectsPage: React.FC = () => {
queryFn: () => getBudgets(1, 100),
});
const { data: allDisbursements = [] } = useQuery({
queryKey: ['disbursements'],
queryFn: () => getDisbursements(1, 200),
});
const { data: allProjectActivities = new Map<string | number, any[]>() } = useQuery({
queryKey: ['allProjectActivities', projects.map((p) => p.id)],
queryFn: async () => {
@@ -311,6 +317,18 @@ export const ProjectsPage: React.FC = () => {
return map;
}, [allBudgets]);
// Disbursed capital per project
const disbursedByProject = useMemo(() => {
const map = new Map<string | number, number>();
allDisbursements.forEach((d) => {
if (Number(d.status) === ApprovalStatus.Approved || Number(d.status) === 3) {
const current = map.get(d.projectId) || 0;
map.set(d.projectId, current + (Number(d.amount) || 0));
}
});
return map;
}, [allDisbursements]);
// Filtered Projects
const filteredProjects = useMemo(() => {
return projects.filter((p) => {
@@ -380,9 +398,22 @@ export const ProjectsPage: React.FC = () => {
return amt > 0 ? formatZAR(amt) : 'R 0';
},
},
{
header: 'Project Variance (ZAR)',
key: 'id',
format: (_, p) => {
const approvedBudget = approvedBudgetsByProject.get(p.id) || 0;
const pActs = allProjectActivities.get(p.id) || [];
const pActsTotal = pActs.reduce((sum, a) => sum + (Number(a.amount) || 0), 0);
const pDisbursedTotal = disbursedByProject.get(p.id) || 0;
const projectValue = pActsTotal - pDisbursedTotal;
const pVariance = approvedBudget - projectValue;
return `${pVariance >= 0 ? '+' : ''}${formatZAR(pVariance)}`;
},
},
{ header: 'Approval Status', key: 'status', format: (val) => getApprovalStatusBadge(val).label },
{ header: 'Created By', key: 'createdBy', format: (val) => getUserEmail(val) },
], [programmeMap, milestonesByProject, approvedBudgetsByProject]);
], [programmeMap, milestonesByProject, approvedBudgetsByProject, allProjectActivities, disbursedByProject]);
// Table Sorting
const customExtractors = {
@@ -404,6 +435,14 @@ export const ProjectsPage: React.FC = () => {
if (risk.isAtRisk) return 2;
return 3;
},
variance: (p: Project) => {
const approvedBudget = approvedBudgetsByProject.get(p.id) || 0;
const pActs = allProjectActivities.get(p.id) || [];
const pActsTotal = pActs.reduce((sum, a) => sum + (Number(a.amount) || 0), 0);
const pDisbursedTotal = disbursedByProject.get(p.id) || 0;
const projectValue = pActsTotal - pDisbursedTotal;
return approvedBudget - projectValue;
},
};
const { sortedData: sortedProjects, sortColumn, sortDirection, handleSort } = useTableSort(
@@ -1007,17 +1046,17 @@ export const ProjectsPage: React.FC = () => {
/>
</div>
</SortableTh>
<th className="py-3.5 px-4 text-right font-semibold whitespace-nowrap w-36">
<SortableTh column="variance" currentColumn={sortColumn} direction={sortDirection} onSort={handleSort} align="right" className="py-3.5 px-4 text-right whitespace-nowrap w-36">
<div className="flex items-center justify-end gap-1">
<span>Project Variance</span>
<HelpTooltip
title="Project Variance"
content="Approved allocated budget minus scheduled milestone activity values. Positive (green) indicates within budget constraints; negative (red) indicates cost overrun."
content="Approved allocated budget minus net project value (milestone activity commitments minus disbursements). Positive (green) indicates remaining budget headroom; negative (red) indicates cost overrun."
size="sm"
placement="bottom"
/>
</div>
</th>
</SortableTh>
<th className="py-3.5 px-4 text-right font-semibold whitespace-nowrap w-56">Actions</th>
</tr>
</thead>
@@ -1212,7 +1251,9 @@ export const ProjectsPage: React.FC = () => {
const approvedBudget = approvedBudgetsByProject.get(p.id) || 0;
const pActs = allProjectActivities.get(p.id) || [];
const pActsTotal = pActs.reduce((sum, a) => sum + (Number(a.amount) || 0), 0);
const pVariance = approvedBudget - pActsTotal;
const pDisbursedTotal = disbursedByProject.get(p.id) || 0;
const projectValue = pActsTotal - pDisbursedTotal;
const pVariance = approvedBudget - projectValue;
const isPos = pVariance >= 0;
return (
<td className="py-3.5 px-4 text-right whitespace-nowrap w-36 font-mono font-bold" onClick={(e) => e.stopPropagation()}>
@@ -1222,7 +1263,7 @@ export const ProjectsPage: React.FC = () => {
? 'text-emerald-400 bg-emerald-500/10 border-emerald-500/30'
: 'text-rose-400 bg-rose-500/10 border-rose-500/30'
}`}
title={`Approved Budget: ${formatZAR(approvedBudget)} | Milestone Activities: ${formatZAR(pActsTotal)} | Variance: ${isPos ? '+' : ''}${formatZAR(pVariance)}`}
title={`Approved Budget: ${formatZAR(approvedBudget)} | Milestone Activities: ${formatZAR(pActsTotal)} | Disbursements: ${formatZAR(pDisbursedTotal)} | Project Value: ${formatZAR(projectValue)} | Variance: ${isPos ? '+' : ''}${formatZAR(pVariance)}`}
>
{isPos ? `+${formatZAR(pVariance)}` : `-${formatZAR(Math.abs(pVariance))}`}
</span>