Fixed UI misalignments
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user