Added, beneficiaries, site visits and change management
This commit is contained in:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user