PartnerUI evidence, notification, indicators and actuals fixes

This commit is contained in:
2026-10-26 08:53:05 +02:00
parent 5ac317cbe3
commit bb77a4dc9a
10 changed files with 1290 additions and 302 deletions
@@ -10,7 +10,7 @@ import {
Organisation,
ApprovalStatus
} from '../../services/api/models';
import { encodeHashIdSync } from '../../services/toolsService';
import { encodeHashIdSync, decodeHashIdSync } from '../../services/toolsService';
import {
toIntegerId,
getMilestones,
@@ -24,6 +24,7 @@ import {
getProjectTypeLabel
} from '../../lib/formatters';
import { EvidenceUploadModal } from './EvidenceUploadModal';
import { GovernanceInfoModal } from './GovernanceInfoModal';
import {
FolderKanban,
ChevronDown,
@@ -87,6 +88,13 @@ export const ProjectCard: React.FC<ProjectCardProps> = ({
activity: Activity;
} | null>(null);
const [governanceTarget, setGovernanceTarget] = useState<{
project: Project;
milestone?: Milestone | null;
activity?: Activity | null;
initialTab?: 'compliance' | 'risks' | 'rules';
} | null>(null);
const { project, award, contract, fundingOrganisation, invitedOrganisation, disbursements } = item;
const hashedProjectId = encodeHashIdSync(project.id);
@@ -136,7 +144,7 @@ export const ProjectCard: React.FC<ProjectCardProps> = ({
};
// Status Badge Helper using Landing Page emerald/teal/amber color palette
const renderStatusBadge = (status?: any, fallback = 'Active') => {
const renderStatusBadge = (status?: any, fallback = 'In Progress') => {
const num = Number(status);
if (num === ApprovalStatus.Approved || String(status).toLowerCase().includes('approved')) {
return (
@@ -154,10 +162,19 @@ export const ProjectCard: React.FC<ProjectCardProps> = ({
</span>
);
}
if (num === ApprovalStatus.Pending || String(status).toLowerCase().includes('pending')) {
return (
<span className="inline-flex items-center gap-1.5 px-2.5 py-0.5 rounded-full text-[11px] font-semibold bg-teal-950/80 border border-teal-500/40 text-teal-300 shadow-sm shadow-teal-950/50">
<span className="w-1.5 h-1.5 rounded-full bg-teal-400 animate-pulse" />
<span>In Progress</span>
</span>
);
}
const label = (status !== undefined && status !== null && !isNaN(num)) ? fallback : String(status || fallback);
return (
<span className="inline-flex items-center gap-1.5 px-2.5 py-0.5 rounded-full text-[11px] font-semibold bg-teal-950/80 border border-teal-500/40 text-teal-300 shadow-sm shadow-teal-950/50">
<span className="w-1.5 h-1.5 rounded-full bg-teal-400 animate-pulse" />
<span>{String(status || fallback)}</span>
<span>{label}</span>
</span>
);
};
@@ -279,7 +296,30 @@ export const ProjectCard: React.FC<ProjectCardProps> = ({
#{hashedProjectId}
</span>
)}
{renderStatusBadge(project.status, 'Active Implementation')}
{hasOverdueMilestone ? (
<span className="inline-flex items-center gap-1.5 px-2.5 py-0.5 rounded-full text-[11px] font-bold bg-rose-950/90 border border-rose-500/50 text-rose-300 shadow-sm shadow-rose-950/50">
<AlertTriangle className="w-3.5 h-3.5 text-rose-400" />
<span>Late (Overdue Milestone)</span>
</span>
) : hasAtRiskMilestone ? (
<span className="inline-flex items-center gap-1.5 px-2.5 py-0.5 rounded-full text-[11px] font-bold bg-amber-950/90 border border-amber-500/50 text-amber-300 shadow-sm shadow-amber-950/50">
<Clock className="w-3.5 h-3.5 text-amber-400" />
<span>At Risk (Milestone Due Soon)</span>
</span>
) : (
renderStatusBadge(project.status, 'Active Implementation')
)}
{/* Requirement #6: Informational Compliance & Risk Modal Trigger */}
<button
type="button"
onClick={() => setGovernanceTarget({ project, initialTab: 'compliance' })}
className="inline-flex items-center gap-1.5 px-3 py-1 rounded-lg bg-white/[0.04] hover:bg-white/[0.08] border border-white/10 text-[11px] font-medium text-slate-300 hover:text-white transition-colors cursor-pointer"
title="View Project Compliance, Risks & Institutional Rules"
>
<ShieldAlert className="w-3.5 h-3.5 text-emerald-400" />
<span>Compliance & Risks</span>
</button>
{/* Requirement #3: Display enum string description instead of integer value */}
{sectorDescription && (
@@ -504,7 +544,11 @@ export const ProjectCard: React.FC<ProjectCardProps> = ({
const evs = getEvidenceForActivity(a.id);
return evs.length > 0 || Number(a.status) === ApprovalStatus.Approved || Number(a.status) === ApprovalStatus.Review;
});
const isMilestoneFullyFulfilled = milestoneActs.length > 0 && milestoneFulfilledActs.length === milestoneActs.length;
const isMilestoneFullyFulfilled = (Number(milestone.status) === ApprovalStatus.Approved) || (milestoneActs.length > 0 && milestoneFulfilledActs.length === milestoneActs.length);
const actualMilestoneId = decodeHashIdSync(milestone.id);
const dueTime = milestone.dueAt ? new Date(milestone.dueAt).getTime() : null;
const isMilestoneOverdue = dueTime !== null && dueTime < nowTime && !isMilestoneFullyFulfilled;
const isMilestoneAtRisk = dueTime !== null && !isMilestoneOverdue && !isMilestoneFullyFulfilled && (dueTime - nowTime <= 3 * 24 * 60 * 60 * 1000);
return (
<div
@@ -521,13 +565,43 @@ export const ProjectCard: React.FC<ProjectCardProps> = ({
<span className="font-mono text-[10px] text-slate-400">
(ID: #{encodeHashIdSync(milestone.id)})
</span>
{renderStatusBadge(milestone.status, 'In Progress')}
{/* Requirement #3: Decode hashId to show actual milestone ID */}
<span className="font-mono text-[10px] font-semibold text-slate-300 bg-slate-900 border border-white/10 px-2 py-0.5 rounded-md">
Actual ID: #{actualMilestoneId}
</span>
{/* Requirement #5: Milestone late and at risk badges */}
{isMilestoneOverdue ? (
<span className="inline-flex items-center gap-1 px-2.5 py-0.5 rounded-full text-[10px] font-bold bg-rose-950/90 border border-rose-500/50 text-rose-300 shadow-sm">
<AlertTriangle className="w-3 h-3 text-rose-400" />
<span>Late</span>
</span>
) : isMilestoneAtRisk ? (
<span className="inline-flex items-center gap-1 px-2.5 py-0.5 rounded-full text-[10px] font-bold bg-amber-950/90 border border-amber-500/50 text-amber-300 shadow-sm">
<Clock className="w-3 h-3 text-amber-400" />
<span>At Risk (Due Soon)</span>
</span>
) : (
renderStatusBadge(milestone.status, 'In Progress')
)}
{isMilestoneFullyFulfilled && (
<span className="inline-flex items-center gap-1 px-2.5 py-0.5 rounded-full text-[10px] font-bold bg-emerald-500/15 text-emerald-300 border border-emerald-500/30">
<ShieldCheck className="w-3 h-3 text-emerald-400" />
<span>Milestone Fulfilled</span>
</span>
)}
{/* Requirement #6: Milestone level Rules & Risks trigger */}
<button
type="button"
onClick={() => setGovernanceTarget({ project, milestone, initialTab: 'rules' })}
className="inline-flex items-center gap-1 px-2 py-0.5 rounded-md bg-white/[0.04] hover:bg-white/[0.08] text-[10px] font-medium text-slate-400 hover:text-white transition-colors cursor-pointer border border-white/5"
title="View Milestone Rules and Risk Guidance"
>
<Scale className="w-3 h-3 text-teal-400" />
<span>Rules & Risks</span>
</button>
</div>
<h5 className="text-base font-bold text-slate-100 mt-1">
{milestone.name || `Milestone Target #${mIdx + 1}`}