import React, { useState, useMemo } from 'react'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { Search, Bell, LogOut, Check, Calendar, FileText, DollarSign, Upload, Send, ChevronDown, ShieldCheck, MessageSquare, FileCheck2, X, Plus, ExternalLink, CheckCircle2, RefreshCw, Briefcase, Award as AwardIcon, } from 'lucide-react'; import { useAuth } from '../../auth/AuthProvider'; import { useRbac } from '../../auth/rbac'; import { useToast } from '../ui/Toast'; import { PfmPartnerLogo } from '../common/PfmPartnerLogo'; import { useInAppNotifications } from '../../hooks/useInAppNotifications'; import { getApplicantData, getMilestones, getProjectEvidence, getActivities, getDisbursements, uploadEvidenceFile, createActivity, createDisbursement, createMilestone, uploadContractFile, executeContract, getContractSecureLink, getEvidenceSecureLink, } from '../../services/api'; import { ApplicantData, Project, Milestone, Evidence, Activity, Disbursement, Contract, Award, Organisation, ApprovalStatus, ActivityStatus, ContractStatus, EvidenceType, Notification, NotificationPlatform, NotificationStatus, } from '../../types'; import { formatZAR } from '../../lib/formatters'; interface EnrichedPartnerProject { project: Project; award: Award | null; contract: Contract | null; fundingOrganisation: Organisation | null; invitedOrganisation: Organisation | null; milestones: Milestone[]; evidence: Evidence[]; activities: Activity[]; disbursements: Disbursement[]; } type NavTab = 'dashboard' | 'messages' | 'contracts' | 'documents'; type SortOption = 'date_added' | 'grant_amount' | 'days_left' | 'name'; const EVIDENCE_TYPE_LABELS: Record = { [EvidenceType.AttendanceRegister]: 'Attendance Register / Progress Report', [EvidenceType.BankStatement]: 'Bank Statement / Financial Statement', [EvidenceType.CompletionCertificate]: 'Completion Certificate', [EvidenceType.FinancialInvoiceOrReceipt]: 'Invoice / Receipt', [EvidenceType.GeotaggedPhoto]: 'Geotagged Site Photograph', [EvidenceType.InspectionReport]: 'Inspection / Audit Report', [EvidenceType.MeetingMinutes]: 'Meeting Minutes', [EvidenceType.PayrollOrStipendRegister]: 'Payroll / Stipend Register', [EvidenceType.ProcurementDocument]: 'Procurement Document', [EvidenceType.TechnicalSpecificationOrDesign]: 'Deliverable Proof / Technical Design', [EvidenceType.VideoRecording]: 'Video Recording', [EvidenceType.Other]: 'Supporting Document', }; function formatShortDate(dateStr?: string | null): string { if (!dateStr) return 'TBD'; const d = new Date(dateStr); if (isNaN(d.getTime())) return 'TBD'; return d.toLocaleDateString('en-ZA', { day: 'numeric', month: 'short' }); } function formatFullDate(dateStr?: string | null): string { if (!dateStr) return 'Not set'; const d = new Date(dateStr); if (isNaN(d.getTime())) return 'Not set'; return d.toLocaleDateString('en-ZA', { day: 'numeric', month: 'short', year: 'numeric' }); } function calculateDaysLeft(endDateStr?: string | null): number { if (!endDateStr) return 90; const end = new Date(endDateStr).getTime(); if (isNaN(end)) return 90; const diff = Math.ceil((end - Date.now()) / (1000 * 60 * 60 * 24)); return Math.max(0, diff); } function getInitials(name?: string | null): string { if (!name) return 'PF'; const parts = name.trim().split(/\s+/); if (parts.length >= 2) { return `${parts[0][0]}${parts[1][0]}`.toUpperCase(); } return name.slice(0, 2).toUpperCase(); } export function DashboardPage() { const { user, sub, logout } = useAuth(); const { canAdd } = useRbac(); const { success: showSuccessToast, error: showErrorToast, info: showInfoToast } = useToast(); const queryClient = useQueryClient(); const applicantUserId = sub || user?.id || ''; // Navigation & UI state const [activeTab, setActiveTab] = useState('dashboard'); const [searchQuery, setSearchQuery] = useState(''); const [sortBy, setSortBy] = useState('date_added'); const [showBellDropdown, setShowBellDropdown] = useState(false); // Modals state const [selectedProjectItem, setSelectedProjectItem] = useState(null); const [projectModalTab, setProjectModalTab] = useState<'milestones' | 'evidence' | 'payments' | 'activities' | 'contract'>('milestones'); const [contactTarget, setContactTarget] = useState<{ organisation: Organisation | null; projectTitle?: string; recipientEmail?: string; recipientId?: string; } | null>(null); const [selectedNotification, setSelectedNotification] = useState(null); // Forms inside Project Details Modal const [uploadingEvidence, setUploadingEvidence] = useState(false); const [selectedMilestoneForUpload, setSelectedMilestoneForUpload] = useState(''); const [selectedEvidenceType, setSelectedEvidenceType] = useState(EvidenceType.FinancialInvoiceOrReceipt); const [evidenceFile, setEvidenceFile] = useState(null); const [paymentAmount, setPaymentAmount] = useState(''); const [paymentMilestoneId, setPaymentMilestoneId] = useState(''); const [activityTitle, setActivityTitle] = useState(''); const [activityCategory] = useState('Implementation'); const [activityAmount, setActivityAmount] = useState(''); const [activityDescription, setActivityDescription] = useState(''); const [newMilestoneName, setNewMilestoneName] = useState(''); const [newMilestoneDueAt, setNewMilestoneDueAt] = useState(''); const [contractFile, setContractFile] = useState(null); const [signatureRef, setSignatureRef] = useState(''); // Contact / Message form state const [messageSubject, setMessageSubject] = useState(''); const [messageBody, setMessageBody] = useState(''); // In-App Notifications & Inbox hook const { inboxNotifications, unreadCount, top5Unread, markAsRead, markAllAsRead, sendMessage, isSending, } = useInAppNotifications(); // ============================================================================ // PRIMARY STARTING API CALL: GetIApplicantDataEndpoint // GET /v1/api/communications/applicants/{applicantUserId:Guid}/all // ============================================================================ const { data: applicantDataList = [], isLoading: isLoadingApplicantData, isFetching: isRefreshingApplicantData, refetch: refetchApplicantData, } = useQuery({ queryKey: ['partner-applicant-data', applicantUserId], queryFn: () => getApplicantData(applicantUserId), enabled: Boolean(applicantUserId), staleTime: 30_000, }); // Extract all unique projects, contracts, awards, and organisations from ApplicantData[] const partnerSummary = useMemo(() => { const projectsMap = new Map(); const contractsMap = new Map(); const awardsList: Award[] = []; let primaryInvitedOrg: Organisation | null = null; let primaryFundingOrg: Organisation | null = null; for (const record of applicantDataList) { if (record.invitedOrganisation && !primaryInvitedOrg) { primaryInvitedOrg = record.invitedOrganisation; } if (record.fundingOrganisation && !primaryFundingOrg) { primaryFundingOrg = record.fundingOrganisation; } const primaryAward = record.awards && record.awards.length > 0 ? record.awards[0] : null; if (record.awards) { for (const aw of record.awards) { if (!awardsList.some(x => String(x.id) === String(aw.id))) { awardsList.push(aw); } } } if (record.contract && record.contract.id) { contractsMap.set(String(record.contract.id), { contract: record.contract, award: primaryAward, fundingOrganisation: record.fundingOrganisation ?? null, invitedOrganisation: record.invitedOrganisation ?? null, }); } if (record.projects && record.projects.length > 0) { for (const proj of record.projects) { if (!proj || !proj.id) continue; const matchingAward = record.awards?.find(a => String(a.programmeId) === String(proj.programmeId)) || primaryAward; projectsMap.set(String(proj.id), { project: proj, award: matchingAward, contract: record.contract ?? null, fundingOrganisation: record.fundingOrganisation ?? null, invitedOrganisation: record.invitedOrganisation ?? null, }); } } } return { projectEntries: Array.from(projectsMap.values()), contractEntries: Array.from(contractsMap.values()), awardsList, primaryInvitedOrg, primaryFundingOrg, }; }, [applicantDataList]); const projectIdsKey = useMemo( () => partnerSummary.projectEntries.map(e => String(e.project.id)).join(','), [partnerSummary.projectEntries] ); // Downstream queries using the IDs provided by GetIApplicantDataEndpoint const { data: projectDetailsMap = {} } = useQuery< Record< string, { milestones: Milestone[]; evidence: Evidence[]; activities: Activity[]; disbursements: Disbursement[]; } > >({ queryKey: ['partner-project-details', projectIdsKey], queryFn: async () => { const entries = partnerSummary.projectEntries; if (entries.length === 0) return {}; let allDisbursements: Disbursement[] = []; try { allDisbursements = await getDisbursements(1, 100); } catch { allDisbursements = []; } const results = await Promise.all( entries.map(async ({ project }) => { const pid = project.id; const [milestones, evidence, activities] = await Promise.all([ getMilestones(pid).catch(() => [] as Milestone[]), getProjectEvidence(pid).catch(() => [] as Evidence[]), getActivities(pid).catch(() => [] as Activity[]), ]); const projectDisbursements = allDisbursements.filter( d => String(d.projectId) === String(pid) ); return [ String(pid), { milestones, evidence, activities, disbursements: projectDisbursements, }, ] as const; }) ); return Object.fromEntries(results); }, enabled: partnerSummary.projectEntries.length > 0, staleTime: 30_000, }); // Combine into EnrichedPartnerProject[] const enrichedProjects: EnrichedPartnerProject[] = useMemo(() => { return partnerSummary.projectEntries.map(entry => { const details = projectDetailsMap[String(entry.project.id)] || { milestones: [], evidence: [], activities: [], disbursements: [], }; return { ...entry, ...details, }; }); }, [partnerSummary.projectEntries, projectDetailsMap]); // Filter and sort projects for the "Ongoing Projects" section const filteredProjects = useMemo(() => { const q = searchQuery.trim().toLowerCase(); const list = enrichedProjects.filter(item => { if (!q) return true; const titleMatch = (item.project.name || '').toLowerCase().includes(q); const descMatch = (item.project.description || '').toLowerCase().includes(q); const funderMatch = (item.fundingOrganisation?.name || '').toLowerCase().includes(q); return titleMatch || descMatch || funderMatch; }); return [...list].sort((a, b) => { if (sortBy === 'name') { return (a.project.name || '').localeCompare(b.project.name || ''); } if (sortBy === 'grant_amount') { const valA = a.contract?.totalValue || a.award?.amount || 0; const valB = b.contract?.totalValue || b.award?.amount || 0; return valB - valA; } if (sortBy === 'days_left') { return ( calculateDaysLeft(a.project.endedAt || a.contract?.expiresAt) - calculateDaysLeft(b.project.endedAt || b.contract?.expiresAt) ); } // Default: 'date_added' (newest first) const timeA = new Date(a.project.createdAt || 0).getTime(); const timeB = new Date(b.project.createdAt || 0).getTime(); return timeB - timeA; }); }, [enrichedProjects, searchQuery, sortBy]); // Calculate Overview 2x2 metrics matching the template const overviewMetrics = useMemo(() => { let runningProjects = 0; let completedProjects = 0; let pendingPayment = 0; let totalPayment = 0; for (const item of enrichedProjects) { const st = Number(item.project.status); const allMilestonesVerified = item.milestones.length > 0 && item.milestones.every(m => Number(m.status) === ApprovalStatus.Approved); if (st === ActivityStatus.Inactive || allMilestonesVerified) { completedProjects += 1; } else { runningProjects += 1; } for (const d of item.disbursements) { const dStatus = Number(d.status); if (dStatus === ApprovalStatus.Approved) { totalPayment += Number(d.amount || 0); } else if ( dStatus === ApprovalStatus.Pending || dStatus === ApprovalStatus.Review ) { pendingPayment += Number(d.amount || 0); } } } // If no disbursements have been recorded yet, reflect total awarded/contracted grant value in Total Payment if (totalPayment === 0 && pendingPayment === 0) { const totalAwarded = partnerSummary.awardsList.reduce( (sum, a) => sum + Number(a.amount || 0), 0 ); const totalContracted = partnerSummary.contractEntries.reduce( (sum, c) => sum + Number(c.contract.totalValue || 0), 0 ); totalPayment = totalContracted || totalAwarded; } return { runningProjects, completedProjects, pendingPayment, totalPayment, }; }, [enrichedProjects, partnerSummary]); // Keep selectedProjectItem synchronized when queries refetch const activeProjectModalItem = useMemo(() => { if (!selectedProjectItem) return null; return ( enrichedProjects.find( x => String(x.project.id) === String(selectedProjectItem.project.id) ) || selectedProjectItem ); }, [selectedProjectItem, enrichedProjects]); // ============================================================================ // MUTATIONS (Simple Partner Actions with Toast Rules) // ============================================================================ const uploadEvidenceMutation = useMutation({ mutationFn: async () => { if (!activeProjectModalItem || !evidenceFile) { throw new Error('Please choose a file to upload.'); } setUploadingEvidence(true); const formData = new FormData(); formData.append('file', evidenceFile); formData.append('projectId', String(activeProjectModalItem.project.id)); if (selectedMilestoneForUpload) { formData.append('milestoneId', selectedMilestoneForUpload); } formData.append('evidenceType', String(selectedEvidenceType)); return await uploadEvidenceFile(formData); }, onSuccess: () => { setUploadingEvidence(false); setEvidenceFile(null); showSuccessToast('Document Submitted', 'Document uploaded and linked to your project.'); queryClient.invalidateQueries({ queryKey: ['partner-project-details'] }); }, onError: (err) => { setUploadingEvidence(false); showErrorToast(err); }, }); const requestPaymentMutation = useMutation({ mutationFn: async () => { if (!activeProjectModalItem) throw new Error('No project selected.'); const amt = Number(paymentAmount); if (!amt || amt <= 0) throw new Error('Please enter a valid payment amount greater than zero.'); return await createDisbursement({ projectId: activeProjectModalItem.project.id, milestoneId: paymentMilestoneId ? paymentMilestoneId : null, amount: amt, }); }, onSuccess: () => { setPaymentAmount(''); setPaymentMilestoneId(''); showSuccessToast('Payment Requested', 'Your payment request has been submitted for review.'); queryClient.invalidateQueries({ queryKey: ['partner-project-details'] }); }, onError: (err) => showErrorToast(err), }); const logActivityMutation = useMutation({ mutationFn: async () => { if (!activeProjectModalItem) throw new Error('No project selected.'); if (!activityTitle.trim()) throw new Error('Please enter a short title for this update.'); return await createActivity({ projectId: activeProjectModalItem.project.id, category: activityCategory, amount: Number(activityAmount) || 0, title: activityTitle.trim(), description: activityDescription.trim(), performedAt: new Date().toISOString(), }); }, onSuccess: () => { setActivityTitle(''); setActivityAmount(''); setActivityDescription(''); showSuccessToast('Update Saved', 'Project progress update logged.'); queryClient.invalidateQueries({ queryKey: ['partner-project-details'] }); }, onError: (err) => showErrorToast(err), }); const addMilestoneMutation = useMutation({ mutationFn: async () => { if (!activeProjectModalItem) throw new Error('No project selected.'); if (!newMilestoneName.trim() || !newMilestoneDueAt) { throw new Error('Please provide both a milestone name and due date.'); } return await createMilestone({ projectId: activeProjectModalItem.project.id, name: newMilestoneName.trim(), dueAt: newMilestoneDueAt, priority: 1, }); }, onSuccess: () => { setNewMilestoneName(''); setNewMilestoneDueAt(''); showSuccessToast('Milestone Added', 'Milestone added to your project schedule.'); queryClient.invalidateQueries({ queryKey: ['partner-project-details'] }); }, onError: (err) => showErrorToast(err), }); const signContractMutation = useMutation({ mutationFn: async (contract: Contract) => { if (contractFile) { await uploadContractFile(contract.id, contractFile); } await executeContract({ contractId: contract.id, externalSignatureId: signatureRef.trim() || `SIG-${Date.now()}`, signedDocumentUrl: contract.signedDocumentUrl || `s3://contracts/${contract.id}/signed.pdf`, }); }, onSuccess: () => { setContractFile(null); setSignatureRef(''); showSuccessToast('Agreement Executed', 'Contract signed and submitted.'); queryClient.invalidateQueries({ queryKey: ['partner-applicant-data'] }); }, onError: (err) => showErrorToast(err), }); const handleOpenSecureFile = async (type: 'contract' | 'evidence', id: string | number) => { try { const link = type === 'contract' ? await getContractSecureLink(id) : await getEvidenceSecureLink(id); if (link) { window.open(link, '_blank', 'noopener,noreferrer'); } else { showInfoToast('File Unavailable', 'No file URL is attached to this record yet.'); } } catch (err) { showErrorToast(err); } }; const handleSendContactMessage = async (e: React.FormEvent) => { e.preventDefault(); if (!messageSubject.trim() || !messageBody.trim()) { showInfoToast('Missing Fields', 'Please enter both a subject and message.'); return; } try { const targetOrg = contactTarget?.organisation || partnerSummary.primaryFundingOrg; const recipientEmail = contactTarget?.recipientEmail || targetOrg?.email || 'support@pfm.gov.za'; const recipientId = contactTarget?.recipientId || targetOrg?.userId || applicantUserId; await sendMessage({ recipientId: String(recipientId), recipientEmail, recipientName: targetOrg?.name || 'Funding Administrator', subject: messageSubject.trim(), message: messageBody.trim(), platform: NotificationPlatform.InApp, organisationId: targetOrg?.id || 1, }); showSuccessToast('Message Sent', 'Your message has been sent to the funding team.'); setMessageSubject(''); setMessageBody(''); setContactTarget(null); } catch (err) { showErrorToast(err); } }; const openContactModalForProject = (item: EnrichedPartnerProject) => { setContactTarget({ organisation: item.fundingOrganisation || partnerSummary.primaryFundingOrg, projectTitle: item.project.name || 'Grant Project', recipientEmail: item.fundingOrganisation?.email || undefined, recipientId: item.fundingOrganisation?.userId || undefined, }); setMessageSubject(`Enquiry regarding ${item.project.name || 'Project'}`); setMessageBody(''); }; return (
{/* Subtle Ambient Background Glow honoring PartnerUI's emerald/teal/cyan palette */}
{/* ===================================================================== TOP NAVIGATION BAR (Matches Uploaded Template + Dark Emerald Theme) ===================================================================== */}
{/* Left: Brand Logo & Navigation Links */}
{/* Right: Search Input, Notification Bell, Partner Profile & Sign Out */}
setSearchQuery(e.target.value)} placeholder="Search projects..." className="w-full pl-10 pr-4 py-2 rounded-xl bg-slate-900/90 border border-white/[0.08] text-sm text-slate-100 placeholder:text-slate-500 focus:outline-none focus:border-emerald-500/50 focus:ring-1 focus:ring-emerald-500/30 transition-all" /> {searchQuery && ( )}
{/* Notification Bell */}
{showBellDropdown && (
Notifications {unreadCount > 0 && ( {unreadCount} unread )}
{unreadCount > 0 && ( )}
{top5Unread.length === 0 ? (
You are all caught up! No new notifications.
) : ( top5Unread.map(item => ( )) )}
)}
{/* User Avatar & Sign Out */}
{getInitials(user?.name || user?.email)}

{user?.name || user?.email || 'Partner User'}

Partner Role

{/* Mobile Navigation Bar */}
{( [ { id: 'dashboard', label: 'Dashboard' }, { id: 'messages', label: `Messages (${unreadCount})` }, { id: 'contracts', label: 'Contracts' }, { id: 'documents', label: 'Documents' }, ] as { id: NavTab; label: string }[] ).map(tab => ( ))}
{/* ===================================================================== MAIN CONTENT AREA ===================================================================== */}
{isLoadingApplicantData ? (

Loading your Partner Workspace...

Retrieving your projects, funding awards, contracts, and milestones.

) : activeTab === 'dashboard' ? ( /* ================================================================= DASHBOARD VIEW — 2-COLUMN LAYOUT BASED ON UPLOADED TEMPLATE Left (~2/3): "Ongoing Projects" + "Sort by date added" + Cards Right (~1/3): "Overview" (2x2 Grid) + "Inbox" ================================================================= */
{/* LEFT COLUMN: ONGOING PROJECTS (8 cols on xl) */}
{/* Section Header */}

Ongoing Projects

Track your grant milestones, submit proof documents, and request payments simply.

{/* Sort Selector matching "Sort by date added v" in template */}
{/* Project Cards List */} {filteredProjects.length === 0 ? (

{searchQuery ? 'No matching projects found' : 'No active projects assigned yet'}

{searchQuery ? 'Try clearing your search filter to see all your funded projects.' : 'Once your funding organisation registers your approved grant award and project, it will appear right here automatically.'}

{partnerSummary.contractEntries.length > 0 && ( )}
) : (
{filteredProjects.map(item => { const { project, award, contract, fundingOrganisation, milestones, evidence, disbursements, } = item; const grantAmount = contract?.totalValue || award?.amount || 0; const disbursedAmount = disbursements .filter(d => Number(d.status) === ApprovalStatus.Approved) .reduce((sum, d) => sum + Number(d.amount || 0), 0); const daysLeft = calculateDaysLeft(project.endedAt || contract?.expiresAt); const docsSubmitted = evidence.length; // Build 5-node stepper matching the template: // [Project start] --- [Milestone 1] --- [Milestone 2] --- [Milestone 3] --- [Milestone 4] const sortedMilestones = [...milestones].sort( (a, b) => new Date(a.dueAt || 0).getTime() - new Date(b.dueAt || 0).getTime() ); const stepperNodes = [ { id: 'start', label: 'Project start', dateLabel: formatShortDate(project.startedAt || contract?.effectiveAt || project.createdAt), state: 'completed' as 'completed' | 'current' | 'upcoming', }, ...[0, 1, 2, 3].map(idx => { const m = sortedMilestones[idx]; if (!m) { return { id: `m-placeholder-${idx}`, label: `Milestone ${idx + 1}`, dateLabel: 'Planned', state: 'upcoming' as const, }; } const mStatus = Number(m.status); const isDone = mStatus === ApprovalStatus.Approved; const isCurrent = !isDone && (mStatus === ApprovalStatus.Review || idx === 0 || Number(sortedMilestones[idx - 1]?.status) === ApprovalStatus.Approved); return { id: String(m.id), label: m.name || `Milestone ${idx + 1}`, dateLabel: formatShortDate(m.dueAt), state: isDone ? ('completed' as const) : isCurrent ? ('current' as const) : ('upcoming' as const), }; }), ]; const funderName = fundingOrganisation?.name || partnerSummary.primaryFundingOrg?.name || 'Funding Organisation'; return (
{/* LEFT PART OF CARD (9 cols): Title, 4 Metrics Row, Horizontal Milestone Stepper */}
{/* Project Title & Description */}

{project.name || 'Funded Project'}

{Number(project.status) === ActivityStatus.Inactive ? 'Completed' : 'Active Grant'}
{project.description && (

{project.description}

)}
{/* 4 Metric Items Row (Matching template's 4 icon+value+sublabel columns) */}
{/* Metric 1: Total Grant */}

{formatZAR(grantAmount)}

Awarded Grant

{/* Metric 2: Paid / Disbursed */}

{formatZAR(disbursedAmount)}

Funds Received

{/* Metric 3: Days Left */}

{daysLeft} days

Days left

{/* Metric 4: Documents / Deliverables Submitted */}

{docsSubmitted} docs

Proof uploaded

{/* Horizontal Milestone Stepper (Matching template's Project start -> Milestone 1..4) */}
{stepperNodes.map((node, idx) => { const isLast = idx === stepperNodes.length - 1; const nextNode = stepperNodes[idx + 1]; const lineCompleted = node.state === 'completed' && (nextNode?.state === 'completed' || nextNode?.state === 'current'); return (
{/* Node Circle + Connecting Line */}
{node.state === 'completed' ? (
) : node.state === 'current' ? (
) : (
)} {!isLast && (
)}
{/* Node Title & Date */}

{node.label}

{node.dateLabel}

); })}
{/* RIGHT PART OF CARD (3 cols): Funder Info + "Contact" & "View Details" Buttons */}
{/* Funder Profile Block */}
{getInitials(funderName)}

{funderName}

{fundingOrganisation?.email || 'Funding Partner'}

Verified Grantor
{/* Stacked Action Buttons: Contact & View Details */}
); })}
)}
{/* RIGHT COLUMN: OVERVIEW (2x2 GRID) + INBOX (4 cols on xl) */}
{/* 1. OVERVIEW CARD */}

Overview

{/* Top-Left: Running Projects */}

{overviewMetrics.runningProjects}

Running Projects

{/* Top-Right: Pending Payment */}

{formatZAR(overviewMetrics.pendingPayment)}

Pending Payment

{/* Bottom-Left: Completed Projects */}

{overviewMetrics.completedProjects}

Completed Projects

{/* Bottom-Right: Total Payment */}

{formatZAR(overviewMetrics.totalPayment)}

Total Payment

{/* 2. INBOX CARD */}

Inbox

{unreadCount > 0 && ( {unreadCount} )}
{inboxNotifications.length === 0 ? (

Your inbox is empty

Messages from your funding administrator will appear here.

) : ( inboxNotifications.slice(0, 6).map(msg => { const isUnread = Number(msg.status) !== NotificationStatus.Read; const senderLabel = msg.destination?.split('|')[0]?.replace('To:', '').trim() || partnerSummary.primaryFundingOrg?.name || 'Funding Office'; return ( ); }) )}
) : activeTab === 'messages' ? ( /* ================================================================= MESSAGES TAB — Simple Non-Technical Partner Inbox & Compose ================================================================= */

Messages & Communications

Direct communication with your funding programme officers.

{inboxNotifications.length === 0 ? (
No messages in your inbox yet.
) : ( inboxNotifications.map(msg => { const isUnread = Number(msg.status) !== NotificationStatus.Read; return (
{ markAsRead(msg.id); setSelectedNotification(msg); }} className="p-5 hover:bg-white/[0.03] transition-colors flex items-start justify-between gap-4 cursor-pointer" >
{getInitials(msg.subject || 'PFM')}

{msg.subject || 'Programme Notification'}

{isUnread && ( New )}

{msg.message}

{formatFullDate(msg.createdAt)}
); }) )}
) : activeTab === 'contracts' ? ( /* ================================================================= CONTRACTS & GRANTS TAB — View & Sign Contracts Simply ================================================================= */

Contracts & Approved Grants

Review your approved funding awards and sign or download your grant agreements.

{partnerSummary.contractEntries.length === 0 ? (
No contracts have been issued to your organisation yet.
) : ( partnerSummary.contractEntries.map(({ contract, award, fundingOrganisation }) => { const isExecuted = Number(contract.status) === ContractStatus.Executed; const contractVer = (contract as any).version || 1; return (
Agreement v{contractVer} {isExecuted ? 'Signed & Executed' : 'Action Required: Review & Sign'}

{fundingOrganisation?.name || 'Grant Funding Agreement'}

Contract Value

{formatZAR(contract.totalValue || award?.amount || 0)}

Valid Period

{formatShortDate(contract.effectiveAt)} – {formatFullDate(contract.expiresAt)}

{!isExecuted && canAdd('grant_financials') && ( )}
); }) )}
) : ( /* ================================================================= DELIVERABLES & DOCUMENTS TAB — All Uploaded Evidence Across Projects ================================================================= */

Deliverables & Proof Documents

All reports, invoices, and milestone proof files submitted across your projects.

{enrichedProjects.flatMap(p => p.evidence.map(ev => ({ ev, project: p.project })) ).length === 0 ? (

No documents uploaded yet. Open any project on your Dashboard and click "View Details" to upload proof documents.

) : (
{enrichedProjects .flatMap(p => p.evidence.map(ev => ({ ev, project: p.project }))) .map(({ ev, project }) => { const evType = Number((ev as any).evidenceType ?? ev.type); const evDate = (ev as any).submittedAt ?? ev.createdAt; return (

{EVIDENCE_TYPE_LABELS[evType] || 'Supporting Document'}{' '} (v{ev.version || 1})

Project: {project.name} • Submitted {formatFullDate(evDate)}

); })}
)}
)}
{/* ===================================================================== MODAL 1: PROJECT WORKSPACE ("View Details" button on Project Card) Simple, non-technical tabs for Milestones, Proof Upload, Payments, Updates ===================================================================== */} {activeProjectModalItem && (
{/* Modal Header */}
Project Workspace Funder: {activeProjectModalItem.fundingOrganisation?.name || 'Funding Partner'}

{activeProjectModalItem.project.name}

{activeProjectModalItem.project.description && (

{activeProjectModalItem.project.description}

)}
{/* Simple Step-by-Step Tabs */}
{( [ { id: 'milestones', label: `1. Milestones (${activeProjectModalItem.milestones.length})` }, { id: 'evidence', label: `2. Upload Proof (${activeProjectModalItem.evidence.length})` }, { id: 'payments', label: `3. Request Payment (${activeProjectModalItem.disbursements.length})` }, { id: 'activities', label: `4. Progress Updates (${activeProjectModalItem.activities.length})` }, { id: 'contract', label: '5. Grant Agreement' }, ] as const ).map(t => ( ))}
{/* Modal Body */}
{projectModalTab === 'milestones' && (

Project Milestones

{activeProjectModalItem.milestones.length === 0 ? (

No milestones have been added to this project yet. You can add one below.

) : (
{activeProjectModalItem.milestones.map((m, idx) => { const isVerified = Number(m.status) === ApprovalStatus.Approved; return (
{isVerified ? : idx + 1}

{m.name}

Due: {formatFullDate(m.dueAt)}

{isVerified ? 'Verified' : 'In Progress'}
); })}
)}
{canAdd('projects_milestones') && (

Add a New Milestone

setNewMilestoneName(e.target.value)} placeholder="Milestone title (e.g. Phase 1 Delivery)" className="sm:col-span-2 px-3.5 py-2 rounded-xl bg-slate-950 border border-white/10 text-xs text-white" /> setNewMilestoneDueAt(e.target.value)} className="px-3.5 py-2 rounded-xl bg-slate-950 border border-white/10 text-xs text-white" />
)}
)} {projectModalTab === 'evidence' && (
{/* Simple Upload Box */}

Upload Supporting Document or Proof

Attach invoices, progress reports, photos, or certificates to prove milestone completion.

setEvidenceFile(e.target.files?.[0] || null)} className="text-xs text-slate-300 file:mr-3 file:py-2 file:px-4 file:rounded-xl file:border-0 file:text-xs file:font-semibold file:bg-emerald-500/15 file:text-emerald-300 hover:file:bg-emerald-500/25" />
{/* Existing Evidence List */}

Submitted Documents ({activeProjectModalItem.evidence.length})

{activeProjectModalItem.evidence.length === 0 ? (

No documents uploaded for this project yet.

) : ( activeProjectModalItem.evidence.map(ev => { const evType = Number((ev as any).evidenceType ?? ev.type); const evDate = (ev as any).submittedAt ?? ev.createdAt; return (

{EVIDENCE_TYPE_LABELS[evType] || 'Document'} (v{ev.version || 1})

Uploaded {formatFullDate(evDate)}

); }) )}
)} {projectModalTab === 'payments' && (
{/* Request Payment Box */}

Request a Grant Payment Drawdown

Select the completed milestone and enter the amount you are requesting.

setPaymentAmount(e.target.value)} placeholder="e.g. 50000" className="w-full px-3.5 py-2 rounded-xl bg-slate-950 border border-white/10 text-xs text-white" />
{/* Payment History */}

Payment Requests & Disbursements

{activeProjectModalItem.disbursements.length === 0 ? (

No payment requests recorded for this project yet.

) : ( activeProjectModalItem.disbursements.map(d => { const isApproved = Number(d.status) === ApprovalStatus.Approved; return (

{formatZAR(d.amount)}

Requested {formatFullDate(d.createdAt)}

{isApproved ? 'Approved & Paid' : 'Pending Review'}
); }) )}
)} {projectModalTab === 'activities' && (

Post a Quick Progress Update

setActivityTitle(e.target.value)} placeholder="What did your team accomplish?" className="sm:col-span-2 px-3.5 py-2 rounded-xl bg-slate-950 border border-white/10 text-xs text-white" /> setActivityAmount(e.target.value)} placeholder="Spend amount (ZAR, optional)" className="px-3.5 py-2 rounded-xl bg-slate-950 border border-white/10 text-xs text-white" />