PartnerUI evidence, notification, indicators and actuals fixes
This commit is contained in:
@@ -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}`}
|
||||
|
||||
Reference in New Issue
Block a user