import React, { useState } from 'react'; import { Project, Award, Milestone, Activity, Evidence } from '../../services/api/models'; import { encodeHashIdSync } from '../../services/toolsService'; import { BorderGlow } from '../common/BorderGlow'; import { EvidenceUploadModal } from './EvidenceUploadModal'; import { FolderKanban, ChevronDown, ChevronUp, Calendar, Landmark, Award as AwardIcon, Layers, CheckCircle2, Clock, FileText, UploadCloud, ExternalLink, ShieldCheck, AlertCircle } from 'lucide-react'; interface ProjectCardProps { project: Project; awards: Award[]; milestones: Milestone[]; activities: Activity[]; evidenceList: Evidence[]; onEvidenceUploaded: () => void; } export const ProjectCard: React.FC = ({ project, awards, milestones, activities, evidenceList, onEvidenceUploaded, }) => { const [isExpanded, setIsExpanded] = useState(false); const [activeTab, setActiveTab] = useState<'milestones' | 'awards'>('milestones'); // Evidence upload state const [uploadTarget, setUploadTarget] = useState<{ activity: Activity; milestone?: Milestone; } | null>(null); const hashedProjectId = encodeHashIdSync(project.id); // Group activities and evidence by milestone const getActivitiesForMilestone = (milestoneId?: string | number) => { if (!milestoneId) return activities.filter((a) => !a.milestoneId); return activities.filter((a) => String(a.milestoneId) === String(milestoneId)); }; const getEvidenceForActivity = (activityId?: string | number) => { if (!activityId) return []; return evidenceList.filter((e) => String(e.activityId) === String(activityId)); }; const getEvidenceForMilestone = (milestoneId?: string | number) => { if (!milestoneId) return []; return evidenceList.filter((e) => String(e.milestoneId) === String(milestoneId)); }; // Helper to check if an activity has >= 1 evidence file const isActivityDoneWithEvidence = (activityId?: string | number) => { const ev = getEvidenceForActivity(activityId); return ev.length > 0; }; // Helper to format currency const formatCurrency = (amount?: number | string | null) => { if (amount === undefined || amount === null || amount === '') return 'R 0.00'; const num = typeof amount === 'number' ? amount : parseFloat(String(amount)); if (isNaN(num)) return 'R 0.00'; return `R ${num.toLocaleString('en-ZA', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`; }; // Helper for status badge rendering const renderStatusBadge = (status?: any, fallback = 'Active') => { const str = String(status || fallback).toLowerCase(); if (str.includes('approved') || str.includes('executed') || str === '2' || str === 'active' || str === 'completed') { return ( {status === 2 || status === '2' ? 'Approved' : String(status || fallback)} ); } if (str.includes('review') || str.includes('pending') || str === '1') { return ( {status === 1 || status === '1' ? 'Pending Approval' : String(status || fallback)} ); } return ( {String(status || fallback)} ); }; // Calculate project progress across all milestones and activities const totalActivities = activities.length; const completedActivitiesCount = activities.filter((a) => isActivityDoneWithEvidence(a.id)).length; const progressPercent = totalActivities > 0 ? Math.round((completedActivitiesCount / totalActivities) * 100) : (milestones.length > 0 ? 0 : 100); const totalAwardsAmount = awards.reduce((sum, a) => sum + (Number(a.amount) || 0), 0); return (
{/* Card Top Header */}
{hashedProjectId && ( #{hashedProjectId} )} {project.code && ( {project.code} )} {renderStatusBadge(project.status, 'In Progress')}

{project.name || `Project #${hashedProjectId}`}

{project.description && (

{project.description}

)}
{/* Key Stats Pill */}
Total Budget
{formatCurrency(project.budget || totalAwardsAmount)}
Linked Awards
{awards.length} Award{awards.length !== 1 ? 's' : ''}
{/* Meta Bar: Dates & Milestone Progress */}
{/* Timeline */}
Timeline {project.startedAt ? new Date(project.startedAt).toLocaleDateString() : 'Start Pending'} —{' '} {project.endedAt ? new Date(project.endedAt).toLocaleDateString() : 'Open'}
{/* Milestones & Activities count */}
Deliverables {milestones.length} Milestone{milestones.length !== 1 ? 's' : ''} • {totalActivities} Activit{totalActivities !== 1 ? 'ies' : 'y'}
{/* Evidence Verification Progress */}
Evidence Completion: {progressPercent}%
{/* Expand / Collapse Button */}
{/* Expanded Section */} {isExpanded && (
{/* TAB 1: MILESTONES & ACTIVITIES */} {activeTab === 'milestones' && (

Milestone Deliverables & Activity Verification

Upload 1 or more evidence files against each activity. A milestone is verified when all its activities have evidence.

{milestones.length === 0 ? (
No milestones recorded for this project yet.
) : (
{milestones.map((milestone) => { const milestoneActs = getActivitiesForMilestone(milestone.id); const milestoneEvidence = getEvidenceForMilestone(milestone.id); const actsWithEvidence = milestoneActs.filter((a) => isActivityDoneWithEvidence(a.id)); const isMilestoneFullyEvidenced = milestoneActs.length > 0 && actsWithEvidence.length === milestoneActs.length; return (
{/* Milestone Header */}
Milestone #{encodeHashIdSync(milestone.id)} {renderStatusBadge(milestone.status, 'In Progress')} {isMilestoneFullyEvidenced && ( Evidence Complete )}
{milestone.name || 'Milestone Target'}
{milestone.dueAt && ( Due: {new Date(milestone.dueAt).toLocaleDateString()} )} {actsWithEvidence.length} / {milestoneActs.length} Activities Verified
{/* Nested Activities */}
Activities & Uploaded Evidence
{milestoneActs.length === 0 ? (
No individual activities defined under this milestone.
) : (
{milestoneActs.map((act) => { const actEvidence = getEvidenceForActivity(act.id); const isDone = actEvidence.length > 0; return (
{act.title || `Activity #${encodeHashIdSync(act.id)}`} {isDone ? ( Done ({actEvidence.length} Evidence) ) : ( Pending Evidence )}
{act.description && (

{act.description}

)}
{/* Upload Evidence Button */}
{/* List of uploaded evidence records against this activity */} {actEvidence.length > 0 && (
Attached Deliverables ({actEvidence.length}):
{actEvidence.map((ev) => (
{ev.title || ev.fileName || `Evidence #${encodeHashIdSync(ev.id)}`}
{ev.documentUrl && ( )}
))}
)}
); })}
)}
); })}
)}
)} {/* TAB 2: AWARDS */} {activeTab === 'awards' && (

Grantee Awards (Historic & Current)

Official grant award commitments linked to this project portfolio.

{awards.length === 0 ? (
No grant awards linked to this project.
) : (
{awards.map((award) => (
Award #{encodeHashIdSync(award.id)} {renderStatusBadge(award.status, 'Active')}
Award Amount
{formatCurrency(award.amount)}
Created: {award.createdAt ? new Date(award.createdAt).toLocaleDateString() : 'N/A'} {award.programmeId && ( Prog #{encodeHashIdSync(award.programmeId)} )}
))}
)}
)}
)}
{/* Modal for Evidence Upload */} {uploadTarget && ( setUploadTarget(null)} project={project} milestone={uploadTarget.milestone} activity={uploadTarget.activity} onEvidenceUploaded={() => { onEvidenceUploaded(); setUploadTarget(null); }} /> )}
); };