Fixed partnerui hashing and over all theming

This commit is contained in:
2026-10-25 19:44:46 +02:00
parent 0bcc3484dc
commit 5ac317cbe3
11 changed files with 341 additions and 273 deletions
@@ -18,7 +18,11 @@ import {
getProjectEvidence,
submitActivityForApproval
} from '../../services/api';
import { getEvidenceTypeLabel } from '../../lib/formatters';
import {
getEvidenceTypeLabel,
getIndustrySectorLabel,
getProjectTypeLabel
} from '../../lib/formatters';
import { EvidenceUploadModal } from './EvidenceUploadModal';
import {
FolderKanban,
@@ -76,7 +80,7 @@ export const ProjectCard: React.FC<ProjectCardProps> = ({
const [submittingActivityId, setSubmittingActivityId] = useState<string | number | null>(null);
// Evidence upload modal state (Requirement #3: Allow user to add evidence, once uploaded refresh data)
// Evidence upload modal state
const [uploadTarget, setUploadTarget] = useState<{
project: Project;
milestone?: Milestone | null;
@@ -120,7 +124,7 @@ export const ProjectCard: React.FC<ProjectCardProps> = ({
}
};
// Date & Time formatter (Requirement #2: sorted by time, displaying time)
// Date & Time formatter
const formatDateTime = (dateStr?: string | null): string => {
if (!dateStr) return 'TBD';
try {
@@ -131,12 +135,12 @@ export const ProjectCard: React.FC<ProjectCardProps> = ({
}
};
// Status Badge Helper
// Status Badge Helper using Landing Page emerald/teal/amber color palette
const renderStatusBadge = (status?: any, fallback = 'Active') => {
const num = Number(status);
if (num === ApprovalStatus.Approved || String(status).toLowerCase().includes('approved')) {
return (
<span className="inline-flex items-center gap-1 px-2.5 py-0.5 rounded-full text-[11px] font-semibold bg-emerald-950/80 border border-emerald-500/30 text-emerald-300">
<span className="inline-flex items-center gap-1.5 px-2.5 py-0.5 rounded-full text-[11px] font-semibold bg-emerald-950/80 border border-emerald-500/40 text-emerald-300 shadow-sm shadow-emerald-950/50">
<CheckCircle2 className="w-3 h-3 text-emerald-400" />
<span>Approved</span>
</span>
@@ -144,23 +148,21 @@ export const ProjectCard: React.FC<ProjectCardProps> = ({
}
if (num === ApprovalStatus.Review || String(status).toLowerCase().includes('review')) {
return (
<span className="inline-flex items-center gap-1 px-2.5 py-0.5 rounded-full text-[11px] font-semibold bg-amber-950/80 border border-amber-500/30 text-amber-300">
<span className="inline-flex items-center gap-1.5 px-2.5 py-0.5 rounded-full text-[11px] font-semibold bg-amber-950/80 border border-amber-500/40 text-amber-300 shadow-sm shadow-amber-950/50">
<Clock className="w-3 h-3 text-amber-400" />
<span>Under Review</span>
</span>
);
}
return (
<span className="inline-flex items-center gap-1 px-2.5 py-0.5 rounded-full text-[11px] font-semibold bg-slate-800/80 border border-slate-700 text-slate-300">
<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>
);
};
// =========================================================================
// ON-DEMAND FETCH: WHEN "View details" IS CLICKED, FETCH USING projectId ON HAND
// (Followup Requirement #1 & #2)
// =========================================================================
// On-demand fetch when expanded
const {
data: projectDetails,
isLoading: isLoadingDetails,
@@ -195,12 +197,11 @@ export const ProjectCard: React.FC<ProjectCardProps> = ({
staleTime: 1000 * 20,
});
// Effective data sources (prioritize freshly fetched projectDetails, fallback to item arrays)
const effectiveMilestones: Milestone[] = projectDetails?.milestones ?? (item.milestones?.length ? item.milestones : []);
const effectiveActivities: Activity[] = projectDetails?.activities ?? (item.activities?.length ? item.activities : []);
const effectiveEvidence: Evidence[] = projectDetails?.evidence ?? (item.evidence?.length ? item.evidence : []);
// 1. Sort Milestones according to Start Date (or createdAt) then Due Date (Requirement #5)
// Sort Milestones according to Start Date then Due Date
const sortedMilestones = [...effectiveMilestones].sort((a, b) => {
const startA = new Date(a.createdAt || a.dueAt || 0).getTime();
const startB = new Date(b.createdAt || b.dueAt || 0).getTime();
@@ -208,7 +209,6 @@ export const ProjectCard: React.FC<ProjectCardProps> = ({
return new Date(a.dueAt || 0).getTime() - new Date(b.dueAt || 0).getTime();
});
// 2. Fetch project evidence, list existing evidence under matching activity by activityId sorted by time (Requirement #2)
const getEvidenceForActivity = (activityId: string | number): Evidence[] => {
const targetNum = toIntegerId(activityId);
return effectiveEvidence
@@ -216,7 +216,6 @@ export const ProjectCard: React.FC<ProjectCardProps> = ({
.sort((e1, e2) => new Date(e2.createdAt || 0).getTime() - new Date(e1.createdAt || 0).getTime());
};
// 3. Nested activities matching MilestoneId sorted by date (Requirement #6)
const getActivitiesForMilestone = (milestoneId: string | number): Activity[] => {
const targetMilestoneNum = toIntegerId(milestoneId);
return effectiveActivities
@@ -228,7 +227,6 @@ export const ProjectCard: React.FC<ProjectCardProps> = ({
});
};
// Unassigned activities
const unassignedActivities = effectiveActivities
.filter(a => !a.milestoneId || toIntegerId(a.milestoneId) === 0)
.sort((a, b) => {
@@ -237,7 +235,6 @@ export const ProjectCard: React.FC<ProjectCardProps> = ({
return dateA - dateB;
});
// Evidence fulfillment progress
const totalActivitiesCount = effectiveActivities.length;
const fulfilledActivitiesCount = effectiveActivities.filter(a => {
const evs = getEvidenceForActivity(a.id);
@@ -248,7 +245,6 @@ export const ProjectCard: React.FC<ProjectCardProps> = ({
? Math.round((fulfilledActivitiesCount / totalActivitiesCount) * 100)
: (effectiveMilestones.length > 0 ? 0 : 100);
// Handle submit activity for approval
const handleSubmitActivity = async (activity: Activity, milestone?: Milestone | null) => {
const evs = getEvidenceForActivity(activity.id);
setSubmittingActivityId(activity.id);
@@ -262,32 +258,38 @@ export const ProjectCard: React.FC<ProjectCardProps> = ({
}
};
// Enum String Descriptions (Requirement #3)
const sectorDescription = getIndustrySectorLabel(project.sector);
const projectTypeDescription = getProjectTypeLabel(project.projectType);
return (
<div className="w-full rounded-[16px] bg-[#181622] border border-white/[0.06] hover:border-white/[0.12] transition-all p-6 sm:p-7 shadow-sm">
<div className="w-full rounded-2xl bg-slate-900/70 border border-white/10 hover:border-emerald-500/30 transition-all p-6 sm:p-7 shadow-xl shadow-black/20 backdrop-blur-xl">
{/* =========================================================================
TOP PORTION: FULL-WIDTH HEADER, METADATA FROM APPLICANT DATA & METRICS
TOP PORTION: FULL-WIDTH HEADER, METADATA & METRICS (Landing Page Theme)
========================================================================= */}
<div className="space-y-5">
{/* Row 1: Badges & Project Title */}
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-3">
<div className="flex items-center gap-2 flex-wrap">
<div className="p-2 rounded-xl bg-purple-500/10 border border-purple-500/20 text-purple-400">
<div className="p-2 rounded-xl bg-emerald-500/10 border border-emerald-500/20 text-emerald-400">
<FolderKanban className="w-4 h-4" />
</div>
{hashedProjectId && (
<span className="font-mono text-xs font-bold text-purple-300 bg-purple-950/70 border border-purple-500/30 px-2.5 py-0.5 rounded-md">
<span className="font-mono text-xs font-bold text-emerald-300 bg-emerald-950/80 border border-emerald-500/30 px-2.5 py-0.5 rounded-md">
#{hashedProjectId}
</span>
)}
{renderStatusBadge(project.status, 'Active Implementation')}
{project.sector && (
<span className="text-[11px] font-medium text-slate-300 bg-white/5 border border-white/10 px-2 py-0.5 rounded-md">
Sector: {project.sector}
{/* Requirement #3: Display enum string description instead of integer value */}
{sectorDescription && (
<span className="text-[11px] font-medium text-emerald-300 bg-emerald-950/60 border border-emerald-500/30 px-2.5 py-0.5 rounded-md">
Sector: {sectorDescription}
</span>
)}
{project.projectType && (
<span className="text-[11px] font-medium text-slate-300 bg-white/5 border border-white/10 px-2 py-0.5 rounded-md">
Type: {project.projectType}
{projectTypeDescription && (
<span className="text-[11px] font-medium text-teal-300 bg-teal-950/60 border border-teal-500/30 px-2.5 py-0.5 rounded-md">
Type: {projectTypeDescription}
</span>
)}
</div>
@@ -296,32 +298,32 @@ export const ProjectCard: React.FC<ProjectCardProps> = ({
<button
type="button"
onClick={toggleExpand}
className="inline-flex items-center gap-2 px-4 py-2 rounded-xl bg-[#8B5CF6]/15 hover:bg-[#8B5CF6]/25 border border-[#8B5CF6]/30 text-[#F1F1F6] text-xs font-semibold transition-all cursor-pointer shadow-sm hover:shadow-[0_0_15px_rgba(139,92,246,0.25)] shrink-0 active:scale-95"
className="inline-flex items-center gap-2 px-4 py-2 rounded-xl bg-emerald-500/15 hover:bg-emerald-500/25 border border-emerald-500/30 text-emerald-300 hover:text-emerald-200 text-xs font-semibold transition-all cursor-pointer shadow-sm hover:shadow-[0_0_15px_rgba(16,185,129,0.25)] shrink-0 active:scale-95"
>
<span>{isExpanded ? 'Hide Details' : 'View Details'}</span>
{isExpanded ? <ChevronUp className="w-4 h-4 text-[#8B5CF6]" /> : <ChevronDown className="w-4 h-4 text-[#8B5CF6]" />}
{isExpanded ? <ChevronUp className="w-4 h-4 text-emerald-400" /> : <ChevronDown className="w-4 h-4 text-emerald-400" />}
</button>
</div>
{/* Row 2: Title and Description */}
<div>
<h3 className="text-xl font-bold text-[#F1F1F6] tracking-tight">
<h3 className="text-xl font-bold text-slate-100 tracking-tight">
{project.name || `Project #${hashedProjectId}`}
</h3>
{project.description && (
<p className="text-[14px] leading-[22px] text-[#9A95B0] mt-1.5 max-w-4xl">
<p className="text-[14px] leading-[22px] text-slate-300 mt-1.5 max-w-4xl">
{project.description}
</p>
)}
</div>
{/* Row 3: Rest of Applicant Data (Funder, Recipient Org, Linked Contract & Award) */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-3 py-3 px-4 rounded-xl bg-white/[0.02] border border-white/5 text-xs">
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-3 py-3 px-4 rounded-xl bg-slate-950/60 border border-white/5 text-xs">
{/* Funder */}
<div className="flex items-center gap-2.5">
<Landmark className="w-4 h-4 text-purple-400 shrink-0" />
<Landmark className="w-4 h-4 text-emerald-400 shrink-0" />
<div className="min-w-0">
<span className="text-[10px] text-[#9A95B0] uppercase font-bold block">Funding Organisation</span>
<span className="text-[10px] text-slate-400 uppercase font-bold block">Funding Organisation</span>
<span className="text-slate-200 font-medium truncate block">
{fundingOrganisation?.name || 'Programme Office'}
</span>
@@ -330,9 +332,9 @@ export const ProjectCard: React.FC<ProjectCardProps> = ({
{/* Recipient Organisation */}
<div className="flex items-center gap-2.5">
<Building className="w-4 h-4 text-emerald-400 shrink-0" />
<Building className="w-4 h-4 text-teal-400 shrink-0" />
<div className="min-w-0">
<span className="text-[10px] text-[#9A95B0] uppercase font-bold block">Partner Recipient</span>
<span className="text-[10px] text-slate-400 uppercase font-bold block">Partner Recipient</span>
<span className="text-slate-200 font-medium truncate block">
{invitedOrganisation?.name || 'Assigned Partner'}
</span>
@@ -343,7 +345,7 @@ export const ProjectCard: React.FC<ProjectCardProps> = ({
<div className="flex items-center gap-2.5">
<FileText className="w-4 h-4 text-cyan-400 shrink-0" />
<div className="min-w-0">
<span className="text-[10px] text-[#9A95B0] uppercase font-bold block">Grant Agreement</span>
<span className="text-[10px] text-slate-400 uppercase font-bold block">Grant Agreement</span>
{contract ? (
<button
type="button"
@@ -364,7 +366,7 @@ export const ProjectCard: React.FC<ProjectCardProps> = ({
<div className="flex items-center gap-2.5">
<AwardIcon className="w-4 h-4 text-amber-400 shrink-0" />
<div className="min-w-0">
<span className="text-[10px] text-[#9A95B0] uppercase font-bold block">Funding Award</span>
<span className="text-[10px] text-slate-400 uppercase font-bold block">Funding Award</span>
<span className="text-amber-300 font-medium font-mono truncate block">
{award ? `Award #${encodeHashIdSync(award.id)}` : 'Approved Award'} ({formatZAR(grantAmount)})
</span>
@@ -376,60 +378,60 @@ export const ProjectCard: React.FC<ProjectCardProps> = ({
<div className="flex flex-wrap items-center justify-between gap-4 pt-2 border-t border-white/5 text-xs">
<div className="flex flex-wrap items-center gap-5 sm:gap-7">
{/* Total Award */}
<div className="flex items-center gap-1.5 font-medium text-[#D1CBDD]">
<span className="p-1 rounded-md bg-purple-500/10 text-purple-400 flex items-center justify-center">
<div className="flex items-center gap-1.5 font-medium text-slate-300">
<span className="p-1 rounded-md bg-emerald-500/10 text-emerald-400 flex items-center justify-center">
<DollarSign className="w-3.5 h-3.5" />
</span>
<span className="font-bold text-[#F1F1F6] font-mono">{formatZAR(grantAmount)}</span>
<span className="text-[#9A95B0]">Award</span>
<span className="font-bold text-slate-100 font-mono">{formatZAR(grantAmount)}</span>
<span className="text-slate-400">Award</span>
</div>
{/* Spend Disbursed */}
<div className="flex items-center gap-1.5 font-medium text-[#D1CBDD]">
<span className="p-1 rounded-md bg-[#F97316]/10 text-[#F97316] flex items-center justify-center">
<div className="flex items-center gap-1.5 font-medium text-slate-300">
<span className="p-1 rounded-md bg-teal-500/10 text-teal-400 flex items-center justify-center">
<DollarSign className="w-3.5 h-3.5" />
</span>
<span className="font-bold text-[#F1F1F6] font-mono">{formatZAR(disbursedAmount)}</span>
<span className="text-[#9A95B0]">Disbursed</span>
<span className="font-bold text-slate-100 font-mono">{formatZAR(disbursedAmount)}</span>
<span className="text-slate-400">Disbursed</span>
</div>
{/* Timeline */}
<div className="flex items-center gap-1.5 font-medium text-[#D1CBDD]">
<span className="p-1 rounded-md bg-amber-500/10 text-amber-400 flex items-center justify-center">
<div className="flex items-center gap-1.5 font-medium text-slate-300">
<span className="p-1 rounded-md bg-cyan-500/10 text-cyan-400 flex items-center justify-center">
<Calendar className="w-3.5 h-3.5" />
</span>
<span className="font-medium text-[#F1F1F6]">
<span className="font-medium text-slate-200">
{formatDate(project.startedAt || contract?.effectiveAt)} – {formatDate(project.endedAt || contract?.expiresAt)}
</span>
<span className="text-[#9A95B0]">({daysLeft})</span>
<span className="text-slate-400">({daysLeft})</span>
</div>
{/* Deliverables Count */}
<div className="flex items-center gap-1.5 font-medium text-[#D1CBDD]">
<span className="p-1 rounded-md bg-teal-500/10 text-teal-400 flex items-center justify-center">
<div className="flex items-center gap-1.5 font-medium text-slate-300">
<span className="p-1 rounded-md bg-slate-800 text-slate-300 flex items-center justify-center">
<Layers className="w-3.5 h-3.5" />
</span>
<span className="font-semibold text-[#F1F1F6]">{effectiveMilestones.length} Milestones</span>
<span className="text-[#9A95B0]">• {totalActivitiesCount} Activities</span>
<span className="font-semibold text-slate-100">{effectiveMilestones.length} Milestones</span>
<span className="text-slate-400">• {totalActivitiesCount} Activities</span>
</div>
{/* Docs Submitted */}
<div className="flex items-center gap-1.5 font-medium text-[#D1CBDD]">
<span className="p-1 rounded-md bg-cyan-500/10 text-cyan-400 flex items-center justify-center">
<div className="flex items-center gap-1.5 font-medium text-slate-300">
<span className="p-1 rounded-md bg-emerald-500/10 text-emerald-400 flex items-center justify-center">
<Paperclip className="w-3.5 h-3.5" />
</span>
<span className="font-bold text-[#F1F1F6] font-mono">{effectiveEvidence.length}</span>
<span className="text-[#9A95B0]">Uploaded Proofs</span>
<span className="font-bold text-slate-100 font-mono">{effectiveEvidence.length}</span>
<span className="text-slate-400">Uploaded Proofs</span>
</div>
</div>
{/* Evidence Completion Progress Pill */}
<div className="flex items-center gap-2">
<span className="text-[11px] text-[#9A95B0]">Fulfillment:</span>
<span className="text-[11px] text-slate-400">Fulfillment:</span>
<span className="text-xs font-bold text-emerald-400 font-mono">{progressPercent}%</span>
<div className="w-20 bg-slate-900 rounded-full h-1.5 overflow-hidden border border-white/10">
<div className="w-20 bg-slate-950 rounded-full h-1.5 overflow-hidden border border-white/10">
<div
className="bg-gradient-to-r from-purple-500 to-emerald-400 h-full rounded-full transition-all duration-500"
className="bg-gradient-to-r from-teal-500 via-emerald-400 to-cyan-300 h-full rounded-full transition-all duration-500"
style={{ width: `${progressPercent}%` }}
/>
</div>
@@ -438,22 +440,21 @@ export const ProjectCard: React.FC<ProjectCardProps> = ({
</div>
{/* =========================================================================
INLINE EXPANDED SECTION: MILESTONES & ACTIVITIES
(Followup Requirements #1, #2, #3)
INLINE EXPANDED SECTION: MILESTONES & ACTIVITIES (Landing Page Palette)
========================================================================= */}
{isExpanded && (
<div className="mt-6 pt-6 border-t border-white/10 space-y-6 animate-in fade-in duration-300">
{/* Section Sub-header & Guidance */}
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-2 bg-[#0F0E17]/80 p-4 rounded-xl border border-white/5">
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-2 bg-slate-950/80 p-4 rounded-xl border border-white/5">
<div>
<h4 className="text-sm font-bold text-white flex items-center gap-2">
<Layers className="w-4 h-4 text-purple-400" />
<h4 className="text-sm font-bold text-slate-100 flex items-center gap-2">
<Layers className="w-4 h-4 text-emerald-400" />
<span>Project Milestones & Contractual Activities</span>
{isFetchingDetails && (
<RefreshCw className="w-3.5 h-3.5 text-purple-400 animate-spin ml-2" />
<RefreshCw className="w-3.5 h-3.5 text-emerald-400 animate-spin ml-2" />
)}
</h4>
<p className="text-[11px] text-[#9A95B0] mt-0.5">
<p className="text-[11px] text-slate-400 mt-0.5">
All milestones are enabled. Complete activities by uploading supporting evidence documents, then submit for review and approval by organisation staff.
</p>
</div>
@@ -462,13 +463,13 @@ export const ProjectCard: React.FC<ProjectCardProps> = ({
type="button"
onClick={() => refetchProjectData()}
disabled={isFetchingDetails}
className="inline-flex items-center gap-1.5 px-3 py-1 rounded-lg bg-white/5 hover:bg-white/10 text-xs text-slate-300 hover:text-white transition-colors cursor-pointer"
className="inline-flex items-center gap-1.5 px-3 py-1 rounded-lg bg-white/5 hover:bg-white/10 text-xs text-slate-300 hover:text-emerald-300 transition-colors cursor-pointer"
title="Refresh project deliverables & evidence"
>
<RefreshCw className={`w-3.5 h-3.5 ${isFetchingDetails ? 'animate-spin text-purple-400' : ''}`} />
<RefreshCw className={`w-3.5 h-3.5 ${isFetchingDetails ? 'animate-spin text-emerald-400' : ''}`} />
<span>Refresh</span>
</button>
<div className="flex items-center gap-1.5 text-[11px] text-purple-300 bg-purple-950/60 border border-purple-500/30 px-3 py-1 rounded-lg">
<div className="flex items-center gap-1.5 text-[11px] text-teal-300 bg-teal-950/60 border border-teal-500/30 px-3 py-1 rounded-lg">
<Info className="w-3.5 h-3.5" />
<span>Fulfillment Only</span>
</div>
@@ -477,18 +478,18 @@ export const ProjectCard: React.FC<ProjectCardProps> = ({
{/* LOADING STATE WHEN VIEW DETAILS IS FIRST CLICKED */}
{isLoadingDetails ? (
<div className="p-8 rounded-2xl bg-[#0F0E17]/60 border border-white/5 flex flex-col items-center justify-center space-y-3 text-xs text-[#9A95B0]">
<Loader2 className="w-6 h-6 text-purple-400 animate-spin" />
<div className="p-8 rounded-2xl bg-slate-950/60 border border-white/5 flex flex-col items-center justify-center space-y-3 text-xs text-slate-400">
<Loader2 className="w-6 h-6 text-emerald-400 animate-spin" />
<span>Fetching project milestones, activities, and evidence...</span>
</div>
) : sortedMilestones.length === 0 ? (
<div className="p-8 rounded-2xl bg-[#0F0E17]/60 border border-white/5 text-center text-xs text-[#9A95B0] space-y-2">
<div className="p-8 rounded-2xl bg-slate-950/60 border border-white/5 text-center text-xs text-slate-400 space-y-2">
<Layers className="w-8 h-8 text-slate-600 mx-auto" />
<p>No contractual milestones recorded for this project yet.</p>
<button
type="button"
onClick={() => refetchProjectData()}
className="inline-flex items-center gap-1 text-purple-400 hover:underline pt-1 cursor-pointer"
className="inline-flex items-center gap-1 text-emerald-400 hover:underline pt-1 cursor-pointer"
>
<RefreshCw className="w-3 h-3" />
<span>Click here to re-check schedule</span>
@@ -508,13 +509,13 @@ export const ProjectCard: React.FC<ProjectCardProps> = ({
return (
<div
key={milestone.id}
className="rounded-2xl border border-white/[0.08] bg-[#0F0E17]/90 p-5 space-y-4 shadow-sm"
className="rounded-2xl border border-white/[0.08] bg-slate-950/90 p-5 space-y-4 shadow-sm"
>
{/* Milestone Header */}
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-3 pb-3 border-b border-white/5">
<div className="space-y-1">
<div className="flex items-center gap-2 flex-wrap">
<span className="font-mono text-xs font-bold text-purple-300 bg-purple-950/70 border border-purple-500/20 px-2.5 py-0.5 rounded-md">
<span className="font-mono text-xs font-bold text-emerald-300 bg-emerald-950/70 border border-emerald-500/30 px-2.5 py-0.5 rounded-md">
Milestone #{mIdx + 1}
</span>
<span className="font-mono text-[10px] text-slate-400">
@@ -528,19 +529,19 @@ export const ProjectCard: React.FC<ProjectCardProps> = ({
</span>
)}
</div>
<h5 className="text-base font-bold text-white mt-1">
<h5 className="text-base font-bold text-slate-100 mt-1">
{milestone.name || `Milestone Target #${mIdx + 1}`}
</h5>
</div>
<div className="flex items-center gap-4 text-xs text-[#9A95B0]">
<div className="flex items-center gap-4 text-xs text-slate-400">
<div>
<span className="text-[10px] text-slate-500 block uppercase font-bold">Start Date</span>
<span className="text-slate-300 font-mono">{formatDate(milestone.createdAt)}</span>
</div>
<div>
<span className="text-[10px] text-slate-500 block uppercase font-bold">Due Date</span>
<span className="text-purple-300 font-bold font-mono">{formatDate(milestone.dueAt)}</span>
<span className="text-emerald-300 font-bold font-mono">{formatDate(milestone.dueAt)}</span>
</div>
<div className="font-mono text-[11px] text-emerald-300 bg-emerald-950/60 border border-emerald-500/20 px-2.5 py-1 rounded-lg">
{milestoneFulfilledActs.length} / {milestoneActs.length} Fulfilled
@@ -548,9 +549,9 @@ export const ProjectCard: React.FC<ProjectCardProps> = ({
</div>
</div>
{/* NESTED ACTIVITIES LISTING (Requirement #1: Fetched for each milestone) */}
{/* NESTED ACTIVITIES LISTING */}
<div className="space-y-3 pt-1">
<div className="text-[11px] font-semibold uppercase tracking-wider text-[#9A95B0] flex items-center justify-between">
<div className="text-[11px] font-semibold uppercase tracking-wider text-slate-400 flex items-center justify-between">
<span>Activities Under Milestone ({milestoneActs.length})</span>
<span className="text-[10px] font-normal normal-case text-slate-500">
Sorted by execution date
@@ -558,7 +559,7 @@ export const ProjectCard: React.FC<ProjectCardProps> = ({
</div>
{milestoneActs.length === 0 ? (
<div className="p-4 rounded-xl bg-white/[0.02] border border-white/5 text-xs text-slate-500 italic">
<div className="p-4 rounded-xl bg-slate-900/40 border border-white/5 text-xs text-slate-500 italic">
No specific activities defined under this milestone.
</div>
) : (
@@ -572,17 +573,17 @@ export const ProjectCard: React.FC<ProjectCardProps> = ({
return (
<div
key={act.id}
className="p-4 rounded-xl bg-white/[0.02] border border-white/5 hover:border-white/10 transition-all space-y-3"
className="p-4 rounded-xl bg-slate-900/40 border border-white/5 hover:border-emerald-500/20 transition-all space-y-3"
>
{/* Activity Details Row */}
<div className="flex flex-col sm:flex-row sm:items-start justify-between gap-3">
<div className="space-y-1.5 min-w-0">
<div className="flex items-center gap-2 flex-wrap">
<span className="text-sm font-semibold text-white">
<span className="text-sm font-semibold text-slate-100">
{act.title || `Activity #${encodeHashIdSync(act.id)}`}
</span>
{act.category && (
<span className="text-[10px] font-medium px-2 py-0.5 rounded-md bg-purple-500/10 border border-purple-500/20 text-purple-300">
<span className="text-[10px] font-medium px-2 py-0.5 rounded-md bg-emerald-500/10 border border-emerald-500/20 text-emerald-300">
{act.category}
</span>
)}
@@ -597,12 +598,12 @@ export const ProjectCard: React.FC<ProjectCardProps> = ({
Submitted • Staff Review Pending
</span>
) : hasEvidence ? (
<span className="inline-flex items-center gap-1 px-2 py-0.5 rounded-md text-[10px] font-bold bg-cyan-950/80 border border-cyan-500/30 text-cyan-300">
<CheckCircle2 className="w-3 h-3 text-cyan-400" />
<span className="inline-flex items-center gap-1 px-2 py-0.5 rounded-md text-[10px] font-bold bg-teal-950/80 border border-teal-500/30 text-teal-300">
<CheckCircle2 className="w-3 h-3 text-teal-400" />
{actEvidences.length} Proof{actEvidences.length > 1 ? 's' : ''} Attached (Ready)
</span>
) : (
<span className="inline-flex items-center gap-1 px-2 py-0.5 rounded-md text-[10px] font-medium bg-slate-900 border border-white/10 text-slate-400">
<span className="inline-flex items-center gap-1 px-2 py-0.5 rounded-md text-[10px] font-medium bg-slate-950 border border-white/10 text-slate-400">
<Clock className="w-3 h-3 text-slate-500" />
Evidence Required
</span>
@@ -610,7 +611,7 @@ export const ProjectCard: React.FC<ProjectCardProps> = ({
</div>
{act.description && (
<p className="text-xs text-[#9A95B0] leading-relaxed">
<p className="text-xs text-slate-300 leading-relaxed">
{act.description}
</p>
)}
@@ -627,24 +628,22 @@ export const ProjectCard: React.FC<ProjectCardProps> = ({
{/* Action Buttons: Add Evidence + Submit for Approval */}
<div className="flex items-center gap-2 shrink-0">
{/* Add Evidence Button (Requirement #3: Allow user to add evidence, once uploaded refresh data) */}
<button
type="button"
onClick={() => setUploadTarget({ project, milestone, activity: act })}
className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-xl bg-purple-500/10 hover:bg-purple-500/20 border border-purple-500/30 text-purple-300 hover:text-purple-200 text-xs font-semibold transition-all cursor-pointer shadow-sm active:scale-95"
className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-xl bg-emerald-500/15 hover:bg-emerald-500/25 border border-emerald-500/30 text-emerald-300 hover:text-emerald-200 text-xs font-semibold transition-all cursor-pointer shadow-sm hover:shadow-[0_0_12px_rgba(16,185,129,0.2)] active:scale-95"
title="Add supporting evidence document for this activity"
>
<Plus className="w-3.5 h-3.5" />
<span>{hasEvidence ? 'Add More Evidence' : 'Add Evidence'}</span>
</button>
{/* Submit for Approval Button */}
{!isApproved && !isUnderReview && (
<button
type="button"
onClick={() => handleSubmitActivity(act, milestone)}
disabled={!hasEvidence || submittingActivityId === act.id}
className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-xl bg-gradient-to-r from-emerald-500 to-teal-500 hover:from-emerald-400 hover:to-teal-400 text-slate-950 text-xs font-bold transition-all shadow-[0_2px_10px_rgba(16,185,129,0.25)] hover:shadow-[0_4px_16px_rgba(16,185,129,0.35)] disabled:opacity-40 disabled:cursor-not-allowed cursor-pointer active:scale-95"
className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-xl bg-gradient-to-r from-emerald-500 via-teal-400 to-cyan-400 hover:from-emerald-400 hover:to-cyan-300 text-slate-950 text-xs font-bold transition-all shadow-[0_2px_12px_rgba(16,185,129,0.35)] hover:shadow-[0_4px_16px_rgba(16,185,129,0.45)] disabled:opacity-40 disabled:cursor-not-allowed cursor-pointer active:scale-95"
title={hasEvidence ? 'Submit uploaded evidence for organisation staff approval' : 'Upload at least one document before submitting for approval'}
>
{submittingActivityId === act.id ? (
@@ -658,7 +657,7 @@ export const ProjectCard: React.FC<ProjectCardProps> = ({
</div>
</div>
{/* LIST EXISTING EVIDENCE UNDER MATCHING ACTIVITY BY activityId SORTED BY TIME, WITH TYPE AND STATUS (Requirement #2) */}
{/* LIST EXISTING EVIDENCE UNDER MATCHING ACTIVITY */}
{actEvidences.length > 0 && (
<div className="pt-2 border-t border-white/5 space-y-2">
<div className="text-[10px] uppercase font-bold tracking-wider text-slate-400 flex items-center justify-between">
@@ -672,16 +671,16 @@ export const ProjectCard: React.FC<ProjectCardProps> = ({
{actEvidences.map((ev) => (
<div
key={ev.id}
className="flex items-center justify-between p-2.5 rounded-lg bg-[#0F0E17] border border-white/10 text-xs"
className="flex items-center justify-between p-2.5 rounded-lg bg-slate-950 border border-white/10 text-xs"
>
<div className="flex items-center gap-2.5 min-w-0 pr-2">
<FileText className="w-4 h-4 text-purple-400 shrink-0" />
<FileText className="w-4 h-4 text-teal-400 shrink-0" />
<div className="min-w-0">
<p className="text-slate-200 font-medium truncate">
{(ev as any).title || ev.fileName || `Evidence #${encodeHashIdSync(ev.id)}`}
</p>
<p className="text-[10px] text-slate-500 truncate">
<span className="font-semibold text-purple-300">
<p className="text-[10px] text-slate-400 truncate">
<span className="font-semibold text-emerald-300">
{getEvidenceTypeLabel(ev.type)}
</span>{' '}
• {formatDateTime(ev.createdAt)}
@@ -694,7 +693,7 @@ export const ProjectCard: React.FC<ProjectCardProps> = ({
<button
type="button"
onClick={() => onOpenSecureFile('evidence', ev.id)}
className="p-1.5 rounded-lg bg-white/5 hover:bg-white/10 text-purple-300 hover:text-purple-200 transition-colors cursor-pointer"
className="p-1.5 rounded-lg bg-white/5 hover:bg-white/10 text-emerald-300 hover:text-emerald-200 transition-colors cursor-pointer"
title="View or download secure file"
>
<ExternalLink className="w-3.5 h-3.5" />
@@ -719,13 +718,13 @@ export const ProjectCard: React.FC<ProjectCardProps> = ({
{/* UNASSIGNED ACTIVITIES */}
{unassignedActivities.length > 0 && (
<div className="rounded-2xl border border-white/[0.08] bg-[#0F0E17]/90 p-5 space-y-4 shadow-sm">
<div className="rounded-2xl border border-white/[0.08] bg-slate-950/90 p-5 space-y-4 shadow-sm">
<div className="border-b border-white/5 pb-2">
<h5 className="text-base font-bold text-white flex items-center gap-2">
<h5 className="text-base font-bold text-slate-100 flex items-center gap-2">
<Layers className="w-4 h-4 text-cyan-400" />
<span>General Project Activities</span>
</h5>
<p className="text-xs text-[#9A95B0] mt-0.5">
<p className="text-xs text-slate-400 mt-0.5">
Activities not bound to a specific milestone. Fulfill by submitting required documents.
</p>
</div>
@@ -740,18 +739,18 @@ export const ProjectCard: React.FC<ProjectCardProps> = ({
return (
<div
key={act.id}
className="p-4 rounded-xl bg-white/[0.02] border border-white/5 hover:border-white/10 transition-all space-y-3"
className="p-4 rounded-xl bg-slate-900/40 border border-white/5 hover:border-emerald-500/20 transition-all space-y-3"
>
<div className="flex flex-col sm:flex-row sm:items-start justify-between gap-3">
<div className="space-y-1 min-w-0">
<div className="flex items-center gap-2 flex-wrap">
<span className="text-sm font-semibold text-white">
<span className="text-sm font-semibold text-slate-100">
{act.title || `Activity #${encodeHashIdSync(act.id)}`}
</span>
{renderStatusBadge(act.status, 'Pending Evidence')}
</div>
{act.description && (
<p className="text-xs text-[#9A95B0] leading-relaxed">
<p className="text-xs text-slate-300 leading-relaxed">
{act.description}
</p>
)}
@@ -765,7 +764,7 @@ export const ProjectCard: React.FC<ProjectCardProps> = ({
<button
type="button"
onClick={() => setUploadTarget({ project, activity: act })}
className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-xl bg-purple-500/10 hover:bg-purple-500/20 border border-purple-500/30 text-purple-300 text-xs font-semibold cursor-pointer"
className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-xl bg-emerald-500/15 hover:bg-emerald-500/25 border border-emerald-500/30 text-emerald-300 text-xs font-semibold cursor-pointer"
>
<Plus className="w-3.5 h-3.5" />
<span>{hasEvidence ? 'Add More Evidence' : 'Add Evidence'}</span>
@@ -776,7 +775,7 @@ export const ProjectCard: React.FC<ProjectCardProps> = ({
type="button"
onClick={() => handleSubmitActivity(act, null)}
disabled={!hasEvidence || submittingActivityId === act.id}
className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-xl bg-emerald-500 hover:bg-emerald-400 text-slate-950 text-xs font-bold disabled:opacity-40 cursor-pointer"
className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-xl bg-gradient-to-r from-emerald-500 via-teal-400 to-cyan-400 hover:from-emerald-400 hover:to-cyan-300 text-slate-950 text-xs font-bold disabled:opacity-40 cursor-pointer"
>
<Send className="w-3.5 h-3.5" />
<span>Submit for Approval</span>
@@ -790,20 +789,20 @@ export const ProjectCard: React.FC<ProjectCardProps> = ({
{actEvidences.map((ev) => (
<div
key={ev.id}
className="flex items-center justify-between p-2.5 rounded-lg bg-[#0F0E17] border border-white/10 text-xs"
className="flex items-center justify-between p-2.5 rounded-lg bg-slate-950 border border-white/10 text-xs"
>
<div className="min-w-0 pr-2">
<span className="text-slate-200 truncate block">
{(ev as any).title || ev.fileName || `Evidence #${encodeHashIdSync(ev.id)}`}
</span>
<span className="text-[10px] text-slate-500">
<span className="text-[10px] text-slate-400">
{getEvidenceTypeLabel(ev.type)} • {formatDateTime(ev.createdAt)}
</span>
</div>
<button
type="button"
onClick={() => onOpenSecureFile('evidence', ev.id)}
className="p-1 rounded text-purple-300 hover:text-purple-200"
className="p-1 rounded text-emerald-300 hover:text-emerald-200"
>
<ExternalLink className="w-3.5 h-3.5" />
</button>
@@ -820,9 +819,7 @@ export const ProjectCard: React.FC<ProjectCardProps> = ({
</div>
)}
{/* =========================================================================
EVIDENCE UPLOAD MODAL (Requirement #3: Allow user to add evidence, once uploaded refresh data)
========================================================================= */}
{/* EVIDENCE UPLOAD MODAL */}
{uploadTarget && (
<EvidenceUploadModal
isOpen={true}