Added, beneficiaries, site visits and change management

This commit is contained in:
2026-09-27 11:40:28 +02:00
parent fb27cc8c92
commit 36e5d3d7c8
16 changed files with 3132 additions and 212 deletions
@@ -24,6 +24,7 @@ import { HelpTooltip } from '../ui/HelpTooltip';
import { useToast } from '../ui/Toast';
import { ExportButton } from '../ui/ExportButton';
import { ExportColumn } from '../../utils/exportUtils';
import { useTableSort, SortableTh } from '../../utils/tableSort';
import {
Instrument,
Instruction,
@@ -237,6 +238,20 @@ export const FundingInstrumentsSection: React.FC<FundingInstrumentsSectionProps>
});
}, [instruments, selectedProgrammeFilter, statusFilter, searchTerm, programmeMap]);
// Sorting hook
const {
sortedData: sortedInstruments,
sortColumn: instSortCol,
sortDirection: instSortDir,
handleSort: handleInstSort,
} = useTableSort<Instrument>(filteredInstruments, null, null, {
code: (inst: Instrument) => inst.code || `INST-${inst.id}`,
name: (inst: Instrument) => inst.name,
programmeId: (inst: Instrument) => programmeMap.get(String(inst.programmeId))?.name || `Programme #${inst.programmeId}`,
status: (inst: Instrument) => getActivityStatusBadge(inst.status).label,
createdAt: (inst: Instrument) => inst.createdAt,
});
// Export columns definition
const instrumentExportColumns: ExportColumn<Instrument>[] = useMemo(
() => [
@@ -400,16 +415,26 @@ export const FundingInstrumentsSection: React.FC<FundingInstrumentsSectionProps>
<table className="w-full text-left border-collapse">
<thead>
<tr className="border-b border-slate-800/80 bg-slate-900/50 text-[11px] font-semibold text-slate-400 uppercase tracking-wider">
<th className="py-3 px-4">Code / Ref</th>
<th className="py-3 px-4">Instrument Title</th>
<th className="py-3 px-4">Associated Programme</th>
<th className="py-3 px-4">Status</th>
<th className="py-3 px-4">Created Info</th>
<SortableTh column="code" currentColumn={instSortCol} direction={instSortDir} onSort={handleInstSort} className="py-3 px-4">
Code / Ref
</SortableTh>
<SortableTh column="name" currentColumn={instSortCol} direction={instSortDir} onSort={handleInstSort} className="py-3 px-4">
Instrument Title
</SortableTh>
<SortableTh column="programmeId" currentColumn={instSortCol} direction={instSortDir} onSort={handleInstSort} className="py-3 px-4">
Associated Programme
</SortableTh>
<SortableTh column="status" currentColumn={instSortCol} direction={instSortDir} onSort={handleInstSort} className="py-3 px-4">
Status
</SortableTh>
<SortableTh column="createdAt" currentColumn={instSortCol} direction={instSortDir} onSort={handleInstSort} className="py-3 px-4">
Created Info
</SortableTh>
<th className="py-3 px-4 text-right">Actions</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-800/60 text-xs text-slate-300">
{filteredInstruments.map((inst) => {
{sortedInstruments.map((inst) => {
const statusBadge = getActivityStatusBadge(inst.status);
const parentProg = programmeMap.get(String(inst.programmeId));
@@ -24,6 +24,7 @@ import { HelpTooltip } from '../ui/HelpTooltip';
import { useToast } from '../ui/Toast';
import { ExportButton } from '../ui/ExportButton';
import { ExportColumn } from '../../utils/exportUtils';
import { useTableSort, SortableTh } from '../../utils/tableSort';
import {
Rule,
Programme,
@@ -233,6 +234,20 @@ export const RulesSection: React.FC<RulesSectionProps> = ({
});
}, [rules, selectedProgrammeFilter, statusFilter, searchTerm, programmeMap]);
// Sorting hook
const {
sortedData: sortedRules,
sortColumn: ruleSortCol,
sortDirection: ruleSortDir,
handleSort: handleRuleSort,
} = useTableSort<Rule>(filteredRules, null, null, {
name: (r: Rule) => r.name,
programmeId: (r: Rule) => programmeMap.get(String(r.programmeId))?.name || `Programme #${r.programmeId}`,
version: (r: Rule) => r.version || 1,
status: (r: Rule) => getActivityStatusBadge(r.status).label,
createdAt: (r: Rule) => r.createdAt,
});
// Export columns definition
const ruleExportColumns: ExportColumn<Rule>[] = useMemo(
() => [
@@ -396,16 +411,26 @@ export const RulesSection: React.FC<RulesSectionProps> = ({
<table className="w-full text-left border-collapse">
<thead>
<tr className="border-b border-slate-800/80 bg-slate-900/50 text-[11px] font-semibold text-slate-400 uppercase tracking-wider">
<th className="py-3 px-4">Rule Title & Directives</th>
<th className="py-3 px-4">Associated Programme</th>
<th className="py-3 px-4">Version</th>
<th className="py-3 px-4">Status</th>
<th className="py-3 px-4">Audit Info</th>
<SortableTh column="name" currentColumn={ruleSortCol} direction={ruleSortDir} onSort={handleRuleSort} className="py-3 px-4">
Rule Title & Directives
</SortableTh>
<SortableTh column="programmeId" currentColumn={ruleSortCol} direction={ruleSortDir} onSort={handleRuleSort} className="py-3 px-4">
Associated Programme
</SortableTh>
<SortableTh column="version" currentColumn={ruleSortCol} direction={ruleSortDir} onSort={handleRuleSort} className="py-3 px-4">
Version
</SortableTh>
<SortableTh column="status" currentColumn={ruleSortCol} direction={ruleSortDir} onSort={handleRuleSort} className="py-3 px-4">
Status
</SortableTh>
<SortableTh column="createdAt" currentColumn={ruleSortCol} direction={ruleSortDir} onSort={handleRuleSort} className="py-3 px-4">
Audit Info
</SortableTh>
<th className="py-3 px-4 text-right">Actions</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-800/60 text-xs text-slate-300">
{filteredRules.map((r) => {
{sortedRules.map((r) => {
const statusBadge = getActivityStatusBadge(r.status);
const parentProg = programmeMap.get(String(r.programmeId));
@@ -11,10 +11,14 @@ import {
Menu,
X,
ChevronRight,
ChevronDown,
ShieldCheck,
Database,
Home,
User,
Users,
GitPullRequest,
MapPin,
} from 'lucide-react';
import { useAuth } from '../../auth/AuthProvider';
import { BrandLogo } from '../ui/BrandLogo';
@@ -23,7 +27,22 @@ import { useQuery } from '@tanstack/react-query';
import { getUsers } from '../../services/api';
import { getUserEmail, registerUserEmail } from '../../lib/formatters';
const navItems = [
interface SubNavItem {
name: string;
path: string;
icon: React.ElementType;
help?: string;
}
interface NavItem {
name: string;
path: string;
icon: React.ElementType;
help: string;
subItems?: SubNavItem[];
}
const navItems: NavItem[] = [
{
name: 'Executive Dashboard',
path: '/dashboard',
@@ -41,6 +60,26 @@ const navItems = [
path: '/projects',
icon: FolderKanban,
help: 'Project Work Breakdown Structure (WBS), contractual deliverables, scheduled milestones, evidence verification, and execution tracking.',
subItems: [
{
name: 'Beneficiaries',
path: '/projects/beneficiaries',
icon: Users,
help: 'Manage target beneficiaries, demographics, and statutory compliance items satisfying beneficiaries.',
},
{
name: 'Change Management',
path: '/projects/change-management',
icon: GitPullRequest,
help: 'Track project issues, contractual change requests, and resolve changes in-place.',
},
],
},
{
name: 'Site Visits',
path: '/site-visits',
icon: MapPin,
help: 'Log physical site visits, field observations, inspector findings, and project feedback.',
},
{
name: 'Grant & Financials',
@@ -151,21 +190,52 @@ export const AppLayout: React.FC = () => {
<nav className="flex-1 overflow-y-auto px-3 py-4 space-y-1.5">
{navItems.map((item) => {
const Icon = item.icon;
const hasSub = item.subItems && item.subItems.length > 0;
return (
<NavLink
key={item.path}
to={item.path}
className={({ isActive }) =>
`flex items-center gap-3 px-3.5 py-2.5 rounded-xl text-xs font-medium transition-all min-w-0 ${
isActive
? 'bg-gradient-to-r from-blue-600 to-indigo-600 text-white shadow-md shadow-blue-600/25 font-semibold'
: 'text-slate-400 hover:text-slate-100 hover:bg-slate-800/60'
}`
}
>
<Icon className="w-4 h-4 shrink-0" />
{sidebarOpen && <span className="truncate">{item.name}</span>}
</NavLink>
<div key={item.path} className="space-y-1">
<NavLink
to={item.path}
end={hasSub}
className={({ isActive }) =>
`flex items-center gap-3 px-3.5 py-2.5 rounded-xl text-xs font-medium transition-all min-w-0 ${
isActive
? 'bg-gradient-to-r from-blue-600 to-indigo-600 text-white shadow-md shadow-blue-600/25 font-semibold'
: 'text-slate-400 hover:text-slate-100 hover:bg-slate-800/60'
}`
}
title={item.name}
>
<Icon className="w-4 h-4 shrink-0" />
{sidebarOpen && <span className="truncate flex-1">{item.name}</span>}
</NavLink>
{/* Sub-items under parent item */}
{hasSub && sidebarOpen && (
<div className="pl-6 pr-1 py-1 space-y-1 border-l border-slate-800/80 ml-5">
{item.subItems?.map((subItem) => {
const SubIcon = subItem.icon;
return (
<NavLink
key={subItem.path}
to={subItem.path}
className={({ isActive }) =>
`flex items-center gap-2.5 px-3 py-2 rounded-lg text-[11px] font-medium transition-all min-w-0 ${
isActive
? 'bg-blue-600/20 text-blue-300 font-semibold border border-blue-500/30'
: 'text-slate-400 hover:text-slate-200 hover:bg-slate-800/50'
}`
}
title={subItem.name}
>
<SubIcon className="w-3.5 h-3.5 shrink-0 text-slate-400" />
<span className="truncate">{subItem.name}</span>
</NavLink>
);
})}
</div>
)}
</div>
);
})}
</nav>
@@ -42,6 +42,7 @@ import { useToast } from '../ui/Toast';
import { ProblemDetailsAlert } from '../ui/ProblemDetailsAlert';
import { ExportButton } from '../ui/ExportButton';
import { ExportColumn } from '../../utils/exportUtils';
import { useTableSort, SortableTh } from '../../utils/tableSort';
import {
Contract,
Award,
@@ -406,6 +407,30 @@ export const ContractsManagement: React.FC<ContractsManagementProps> = ({
{ header: 'Document URI', key: 'fileUri', format: (val) => val || '—' },
];
// Table sorting hook
const {
sortedData: sortedContracts,
sortColumn: contractSortCol,
sortDirection: contractSortDir,
handleSort: handleContractSort,
} = useTableSort<Contract>(filteredContracts, null, null, {
id: (c: any) => c.id,
awardId: (c: any) => c.totalValue || c.awardId,
programmeId: (c: any) => {
const a = getAwardForContract(c);
const pr = getProgrammeForAward(a);
return pr?.name || '';
},
organisationId: (c: any) => {
const a = getAwardForContract(c);
const o = getOrganisationForAward(a);
return o?.name || '';
},
signedDate: (c: any) => c.signedDate,
status: (c: any) => getContractStatusBadge(c.status).label,
fileUri: (c: any) => c.fileUri || '',
});
return (
<div className="space-y-6">
{/* Summary Cards */}
@@ -612,18 +637,32 @@ export const ContractsManagement: React.FC<ContractsManagementProps> = ({
<table className="w-full text-left text-xs text-slate-300">
<thead className="bg-[#151b2c]/80 text-slate-400 border-b border-slate-800/80 uppercase font-semibold text-[11px] tracking-wider">
<tr>
<th className="py-3.5 px-4">Contract / Ref</th>
<th className="py-3.5 px-4">Award & Amount</th>
<th className="py-3.5 px-4">Programme & Project</th>
<th className="py-3.5 px-4">Organisation</th>
<th className="py-3.5 px-4">Validity Period</th>
<th className="py-3.5 px-4">Status</th>
<th className="py-3.5 px-4">Document / S3</th>
<th className="py-3.5 px-4 text-right">Actions</th>
<SortableTh column="id" currentColumn={contractSortCol} direction={contractSortDir} onSort={handleContractSort} className="py-3.5 px-4">
Contract / Ref
</SortableTh>
<SortableTh column="awardId" currentColumn={contractSortCol} direction={contractSortDir} onSort={handleContractSort} className="py-3.5 px-4">
Award & Amount
</SortableTh>
<SortableTh column="programmeId" currentColumn={contractSortCol} direction={contractSortDir} onSort={handleContractSort} className="py-3.5 px-4">
Programme & Project
</SortableTh>
<SortableTh column="organisationId" currentColumn={contractSortCol} direction={contractSortDir} onSort={handleContractSort} className="py-3.5 px-4">
Organisation
</SortableTh>
<SortableTh column="signedDate" currentColumn={contractSortCol} direction={contractSortDir} onSort={handleContractSort} className="py-3.5 px-4">
Validity Period
</SortableTh>
<SortableTh column="status" currentColumn={contractSortCol} direction={contractSortDir} onSort={handleContractSort} className="py-3.5 px-4">
Status
</SortableTh>
<SortableTh column="fileUri" currentColumn={contractSortCol} direction={contractSortDir} onSort={handleContractSort} className="py-3.5 px-4">
Document / S3
</SortableTh>
<th className="py-3.5 px-4 text-right font-medium">Actions</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-800/50">
{filteredContracts.map((contract) => {
{sortedContracts.map((contract) => {
const award = getAwardForContract(contract);
const org = getOrganisationForAward(award);
const prog = getProgrammeForAward(award);
@@ -233,7 +233,7 @@ export const EvidenceReviewQueue: React.FC<EvidenceReviewQueueProps> = ({
if (variables.status === ApprovalStatus.Approved) {
success('Evidence Approved & Verified', 'Approval recorded. Contributes towards contractual milestone sign-off.');
} else if (variables.status === ApprovalStatus.Rejected) {
success('Evidence Rejected', 'Evidence marked as Rejected via verification endpoint.');
success('Evidence Rejected', 'Evidence marked as Rejected by reviewer.');
} else {
success('Status Updated', 'Evidence status updated successfully.');
}
@@ -971,7 +971,7 @@ export const EvidenceReviewQueue: React.FC<EvidenceReviewQueueProps> = ({
</div>
<p className="text-[11px] text-slate-400 leading-relaxed">
Review the deliverable document against contractual requirements. If the evidence does not satisfy requirements, reject the submission using the endpoint action below. Approved evidence validates progress towards milestone completion.
Review the deliverable document against contractual requirements. If the evidence does not satisfy requirements, reject the submission using the review actions below. Approved evidence validates progress towards milestone completion.
</p>
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-3 pt-1">
@@ -73,7 +73,7 @@ export function ExportButton<T>({
};
return (
<div className={`relative inline-block ${className}`} ref={dropdownRef}>
<div className={`relative inline-block ${isOpen ? 'z-[9999]' : 'z-30'} ${className}`} ref={dropdownRef}>
<button
type="button"
onClick={() => !isDisabled && setIsOpen(!isOpen)}
@@ -97,7 +97,7 @@ export function ExportButton<T>({
</button>
{isOpen && !isDisabled && (
<div className="absolute right-0 mt-1.5 w-52 bg-[#0d121f] border border-slate-700/90 rounded-xl shadow-2xl z-50 overflow-hidden backdrop-blur-md animate-in fade-in-50 zoom-in-95">
<div className="absolute right-0 mt-2 w-56 bg-[#0d121f] border border-slate-700/90 rounded-xl shadow-2xl z-[99999] overflow-hidden backdrop-blur-md animate-in fade-in-50 zoom-in-95 ring-1 ring-black/40">
<div className="p-2 border-b border-slate-800/80">
<span className="text-[10px] font-semibold uppercase tracking-wider text-slate-400 px-2 block">
Export Format ({data.length} items)