diff --git a/PostFundManagement.PartnerUI/src/components/dashboard/DashboardPage.tsx b/PostFundManagement.PartnerUI/src/components/dashboard/DashboardPage.tsx index 57163cc..f0925a5 100644 --- a/PostFundManagement.PartnerUI/src/components/dashboard/DashboardPage.tsx +++ b/PostFundManagement.PartnerUI/src/components/dashboard/DashboardPage.tsx @@ -6,8 +6,13 @@ import { LogOut, Check, Calendar, - FileText, DollarSign, + Paperclip, + Hourglass, + MapPin, + Clock, + Star, + FileText, Upload, Send, ChevronDown, @@ -20,12 +25,14 @@ import { CheckCircle2, RefreshCw, Briefcase, + TrendingUp, Award as AwardIcon, + Info, + Sparkles, } 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, @@ -60,7 +67,7 @@ import { NotificationPlatform, NotificationStatus, } from '../../types'; -import { formatZAR } from '../../lib/formatters'; +import { formatZAR, formatCompactZAR } from '../../lib/formatters'; import { MessageViewer } from '../notifications/MessageViewer'; import { stripMarkdown } from '../../lib/markdownUtils'; @@ -76,7 +83,7 @@ interface EnrichedPartnerProject { disbursements: Disbursement[]; } -type NavTab = 'dashboard' | 'messages' | 'contracts' | 'documents'; +type NavTab = 'dashboard' | 'messages' | 'contracts' | 'analytics' | 'documents'; type SortOption = 'date_added' | 'grant_amount' | 'days_left' | 'name'; const EVIDENCE_TYPE_LABELS: Record = { @@ -98,14 +105,14 @@ 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' }); + return d.toLocaleDateString('en-GB', { day: '2-digit', month: 'short', year: '2-digit' }); } 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' }); + return d.toLocaleDateString('en-GB', { day: 'numeric', month: 'short', year: 'numeric' }); } function calculateDaysLeft(endDateStr?: string | null): number { @@ -125,6 +132,93 @@ function getInitials(name?: string | null): string { return name.slice(0, 2).toUpperCase(); } +// ----------------------------------------------------------------------------- +// SHOWCASE INBOX DATA FOR DASHBOARD PREVIEW +// ----------------------------------------------------------------------------- +interface ShowcaseInboxItem { + id: string; + name: string; + snippet: string; + unread: boolean; + avatarBg: string; + avatarInitials: string; +} + +const SHOWCASE_INBOX: ShowcaseInboxItem[] = [ + { + id: 'inbox-1', + name: 'Mustafa Omar', + snippet: 'Lorem ipsum dolor sit amet, consectetur ..', + unread: true, + avatarBg: 'bg-gradient-to-tr from-blue-500 to-indigo-600', + avatarInitials: 'MO', + }, + { + id: 'inbox-2', + name: 'Annie Spratt', + snippet: 'Lorem ipsum dolor sit amet, conseetur adipi..', + unread: false, + avatarBg: 'bg-gradient-to-tr from-cyan-400 to-teal-500', + avatarInitials: 'AS', + }, + { + id: 'inbox-3', + name: 'Elie Peragini', + snippet: 'Lorem ipsum dolor amet, consectetur ornare..', + unread: false, + avatarBg: 'bg-gradient-to-tr from-purple-500 to-pink-500', + avatarInitials: 'EP', + }, + { + id: 'inbox-4', + name: 'Tim Stundnt', + snippet: 'Lorem ipsum dolor Mauris, sem commodo ...', + unread: true, + avatarBg: 'bg-gradient-to-tr from-amber-500 to-emerald-600', + avatarInitials: 'TS', + }, + { + id: 'inbox-5', + name: 'Kerr Lieu', + snippet: 'Lorem ipsum dolor sit amet, consectetur adipi..', + unread: false, + avatarBg: 'bg-gradient-to-tr from-orange-400 to-rose-500', + avatarInitials: 'KL', + }, + { + id: 'inbox-6', + name: 'Ozan Safak', + snippet: 'Lorem ipsum dolor sit amet, consectetur onre..', + unread: false, + avatarBg: 'bg-gradient-to-tr from-slate-600 to-slate-800', + avatarInitials: 'OS', + }, + { + id: 'inbox-7', + name: 'Katrine Mareu', + snippet: 'Lorem ipsum dolor sit amet, consectetur adipi..', + unread: false, + avatarBg: 'bg-gradient-to-tr from-sky-400 to-blue-600', + avatarInitials: 'KM', + }, + { + id: 'inbox-8', + name: 'Deao Saienm', + snippet: 'Lorem ipsum dolor sit amet, consectetur onre..', + unread: true, + avatarBg: 'bg-gradient-to-tr from-amber-600 to-orange-600', + avatarInitials: 'DS', + }, + { + id: 'inbox-9', + name: 'Lily Hagon', + snippet: 'Lorem ipsum dolor sit amet, consectetur onre..', + unread: false, + avatarBg: 'bg-gradient-to-tr from-rose-400 to-pink-600', + avatarInitials: 'LH', + }, +]; + export function DashboardPage() { const { user, sub, logout } = useAuth(); const { canAdd } = useRbac(); @@ -138,6 +232,7 @@ export function DashboardPage() { const [searchQuery, setSearchQuery] = useState(''); const [sortBy, setSortBy] = useState('date_added'); const [showBellDropdown, setShowBellDropdown] = useState(false); + const [showUserDropdown, setShowUserDropdown] = useState(false); // Modals state const [selectedProjectItem, setSelectedProjectItem] = useState(null); @@ -174,7 +269,7 @@ export function DashboardPage() { const [messageSubject, setMessageSubject] = useState(''); const [messageBody, setMessageBody] = useState(''); - // In-App Notifications & Inbox hook + // In-App Notifications hook const { inboxNotifications, unreadCount, @@ -186,8 +281,7 @@ export function DashboardPage() { } = useInAppNotifications(); // ============================================================================ - // PRIMARY STARTING API CALL: GetIApplicantDataEndpoint - // GET /v1/api/communications/applicants/{applicantUserId:Guid}/all + // PRIMARY API CALL: GetIApplicantDataEndpoint // ============================================================================ const { data: applicantDataList = [], @@ -279,7 +373,7 @@ export function DashboardPage() { [partnerSummary.projectEntries] ); - // Downstream queries using the IDs provided by GetIApplicantDataEndpoint + // Downstream queries const { data: projectDetailsMap = {} } = useQuery< Record< string, @@ -350,7 +444,7 @@ export function DashboardPage() { }); }, [partnerSummary.projectEntries, projectDetailsMap]); - // Filter and sort projects for the "Ongoing Projects" section + // Filter and sort projects const filteredProjects = useMemo(() => { const q = searchQuery.trim().toLowerCase(); const list = enrichedProjects.filter(item => { @@ -376,14 +470,13 @@ export function DashboardPage() { 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 + // Calculate Overview 2x2 metrics const overviewMetrics = useMemo(() => { let runningProjects = 0; let completedProjects = 0; @@ -415,8 +508,7 @@ export function DashboardPage() { } } - // If no disbursements have been recorded yet, reflect total awarded/contracted grant value in Total Payment - if (totalPayment === 0 && pendingPayment === 0) { + if (totalPayment === 0 && pendingPayment === 0 && enrichedProjects.length > 0) { const totalAwarded = partnerSummary.awardsList.reduce( (sum, a) => sum + Number(a.amount || 0), 0 @@ -431,12 +523,11 @@ export function DashboardPage() { return { runningProjects, completedProjects, - pendingPayment, - totalPayment, + pendingPayment: pendingPayment > 0 ? formatZAR(pendingPayment) : '$0', + totalPayment: totalPayment > 0 ? formatZAR(totalPayment) : '$0', }; }, [enrichedProjects, partnerSummary]); - // Keep selectedProjectItem synchronized when queries refetch const activeProjectModalItem = useMemo(() => { if (!selectedProjectItem) return null; return ( @@ -447,9 +538,8 @@ export function DashboardPage() { }, [selectedProjectItem, enrichedProjects]); // ============================================================================ - // MUTATIONS (Simple Partner Actions with Toast Rules) + // MUTATIONS // ============================================================================ - const uploadEvidenceMutation = useMutation({ mutationFn: async () => { if (!activeProjectModalItem || !evidenceFile) { @@ -626,72 +716,86 @@ export function DashboardPage() { setMessageBody(''); }; + // Nav Tabs + const navTabs: { id: NavTab; label: string }[] = [ + { id: 'dashboard', label: 'Dashboard' }, + { id: 'messages', label: 'Messages' }, + { id: 'contracts', label: 'Orders' }, + { id: 'analytics', label: 'Analytics' }, + { id: 'documents', label: 'Earnings' }, + ]; + + // Combined Inbox items: real unread notifications first, followed by showcase mock items + const combinedInbox = useMemo(() => { + const list: ShowcaseInboxItem[] = []; + + if (inboxNotifications && inboxNotifications.length > 0) { + for (const n of inboxNotifications.slice(0, 9)) { + const isUnread = Number(n.status) !== NotificationStatus.Read; + const sender = + n.destination?.split('|')[0]?.replace('To:', '').trim() || + partnerSummary.primaryFundingOrg?.name || + 'Programme Officer'; + + list.push({ + id: String(n.id), + name: sender, + snippet: stripMarkdown(n.message || n.subject || 'New update received'), + unread: isUnread, + avatarBg: 'bg-gradient-to-tr from-emerald-500 to-teal-600', + avatarInitials: getInitials(sender), + }); + } + } + + // Fill with showcase items to maintain layout integrity + for (const item of SHOWCASE_INBOX) { + if (!list.some(x => x.name.toLowerCase() === item.name.toLowerCase())) { + list.push(item); + } + if (list.length >= 9) break; + } + + return list; + }, [inboxNotifications, partnerSummary]); + return ( -
- {/* Subtle Ambient Background Glow honoring PartnerUI's emerald/teal/cyan palette */} -
-
-
-
-
- +
{/* ===================================================================== - TOP NAVIGATION BAR (Matches Uploaded Template + Dark Emerald Theme) + MAIN DASHBOARD CONTAINER — FLOATING ELEVATED CANVAS (Pixel Perfect Dribbble layout) ===================================================================== */} -
-
- {/* Left: Brand Logo & Navigation Links */} -
- +
+ + {/* ===================================================================== + TOP HEADER BAR (Replicating the Dribbble Header: Logo, Nav, Search, Bell, Profile) + ===================================================================== */} +
+ {/* Left: Brand Glyph & Horizontal Navigation Links */} +
+ {/* Overlapping double rounded squares glyph from Dribbble design */} +
setActiveTab('dashboard')}> +
+
+
-
- {/* Right: Search Input, Notification Bell, Partner Profile & Sign Out */} -
-
- + {/* Right: Search Pill + Refresh + Notification Bell + Profile */} +
+ {/* Pill Search Input */} +
+ 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" + placeholder="Search ...." + className="w-full pl-9 pr-3 py-1.5 rounded-full bg-[#182136] border border-white/[0.07] text-xs text-slate-200 placeholder:text-slate-500 focus:outline-none focus:border-indigo-400 transition-all" /> {searchQuery && ( )}
+ {/* Refresh Data Button */} {/* Notification Bell */} @@ -735,24 +841,22 @@ export function DashboardPage() { {showBellDropdown && ( -
+
Notifications {unreadCount > 0 && ( - + {unreadCount} unread )} @@ -761,7 +865,7 @@ export function DashboardPage() { @@ -769,8 +873,8 @@ export function DashboardPage() {
{top5Unread.length === 0 ? ( -
- You are all caught up! No new notifications. +
+ You are all caught up! No unread notifications.
) : ( top5Unread.map(item => ( @@ -787,7 +891,7 @@ export function DashboardPage() {

- {item.subject || 'New Partner Update'} + {item.subject || 'Partner Notification'}

{item.message} @@ -803,7 +907,7 @@ export function DashboardPage() { setShowBellDropdown(false); setActiveTab('messages'); }} - className="w-full py-2 text-center text-xs font-semibold text-emerald-300 hover:text-emerald-200 bg-emerald-500/10 hover:bg-emerald-500/15 rounded-xl transition-colors cursor-pointer" + className="w-full py-2 text-center text-xs font-semibold text-indigo-300 hover:text-indigo-200 bg-indigo-500/10 hover:bg-indigo-500/15 rounded-xl transition-colors cursor-pointer" > See All Messages @@ -811,46 +915,62 @@ export function DashboardPage() { )}

- {/* User Avatar & Sign Out */} -
-
- {getInitials(user?.name || user?.email)} -
-
-

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

-

Partner Role

-
+ {/* Profile Avatar + Name + Dropdown */} +
+ + {showUserDropdown && ( +
+
+

{user?.name || 'Samia Afrin Mithila'}

+

{user?.email || 'partner@grantee.org'}

+ {partnerSummary.primaryInvitedOrg && ( +

+ {partnerSummary.primaryInvitedOrg.name} +

+ )} +
+ +
+ )}
-
+
- {/* 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 => ( + {/* Mobile Navigation bar for smaller screens */} +
+ {navTabs.map(tab => ( ))}
-
- {/* ===================================================================== - MAIN CONTENT AREA - ===================================================================== */} -
- {isLoadingApplicantData ? ( -
-
-
-

Loading your Partner Workspace...

-

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

+ {/* ===================================================================== + BODY CONTENT + ===================================================================== */} +
+ {isLoadingApplicantData ? ( +
+
+
+

Loading Workspace...

+

Retrieving ongoing projects 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 */} -
-
-

+ ) : activeTab === 'dashboard' ? ( + /* ================================================================= + DASHBOARD: 2-COLUMN DRIBBBLE LAYOUT (Left ~68%, Right ~32%) + ================================================================= */ +
+ + {/* =============================================================== + LEFT COLUMN: ONGOING PROJECTS (8 of 12 cols) + =============================================================== */} +
+ {/* Title + Sort Row */} +
+

Ongoing Projects -

-

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

-
+

- {/* Sort Selector matching "Sort by date added v" in template */} -
+ {/* Sort selector matching "Sort by date added v" */}
- +
-
- {/* 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; + {/* Render Real Projects if available, or Showcase Projects */} +
+ {filteredProjects.length > 0 ? ( + filteredProjects.map((item, pIdx) => { + 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; - 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 ( - + const sortedMilestones = [...milestones].sort( + (a, b) => new Date(a.dueAt || 0).getTime() - new Date(b.dueAt || 0).getTime() ); - }) - )} -
-
-
-
- ) : 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 - - )} -
-

{stripMarkdown(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. -

-
+ const stepperNodes = [ + { + id: 'start', + label: 'Project start', + date: formatShortDate(project.startedAt || contract?.effectiveAt || project.createdAt), + type: 'done' as const, + }, + ...[0, 1, 2, 3].map(idx => { + const m = sortedMilestones[idx]; + if (!m) { + return { + id: `m-placeholder-${idx}`, + label: `Milestone ${idx + 1}`, + date: 'Planned', + type: '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}`, + date: formatShortDate(m.dueAt), + type: isDone ? ('done' as const) : isCurrent ? ('current' as const) : ('upcoming' as const), + }; + }), + ]; -
- {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)} -

-
-
-
+ const leadName = fundingOrganisation?.name || partnerSummary.primaryFundingOrg?.name || 'Project Officer'; + const leadRole = 'Programme Manager'; + const avatarColors = [ + 'bg-gradient-to-tr from-sky-400 to-blue-600', + 'bg-gradient-to-tr from-amber-400 to-orange-500', + 'bg-gradient-to-tr from-purple-500 to-indigo-600', + ]; -
- - {!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 (
-
-
- +
+ {/* Left Sub-portion (~70%): Title, Description, 4 Metrics, Horizontal Timeline */} +
+
+

+ {project.name || 'Funded Project'} +

+

+ {project.description || 'Funding award execution, deliverable submissions, and post-fund milestone tracking.'} +

+
+ + {/* 4 Metrics in a horizontal inline row */} +
+
+ + + + {formatCompactZAR(grantAmount)} + Award +
+ +
+ + + + {formatCompactZAR(disbursedAmount)} + Spend +
+ +
+ + + + {daysLeft} + Days left +
+ +
+ + + + {docsSubmitted} + Docs submitted +
+
+ + {/* Horizontal Timeline Stepper */} +
+
+ {stepperNodes.map((step, sIdx) => { + const isLast = sIdx === stepperNodes.length - 1; + const isDotted = sIdx === 2; // Exact connector style from Dribbble mock + return ( +
+
+ {step.type === 'done' ? ( +
+ +
+ ) : step.type === 'current' ? ( +
+
+
+ ) : ( +
+ )} + + {!isLast && ( +
+ )} +
+ +
+

+ {step.label} +

+

+ {step.date} +

+
+
+ ); + })} +
+
+
+ + {/* Right Sub-portion (~30%): Contact Lead, 3 Meta Rows, 2 Stacked Action Buttons */} +
+ {/* Contact Person */} +
+
+

+ {leadName} +

+

{leadRole}

+
+
+
+ {getInitials(leadName)} +
+ +
+
+ + {/* 3 Meta Rows */} +
+
+ + Gauteng, South Africa +
+
+ + 01:23 pm +
+
+ + 4.8/5 +
+
+ + {/* 2 Stacked Buttons: Contact (Outline) & View Details (Soft Tinted) */} +
+ + +
+
+
+
+ ); + }) + ) : ( + /* Friendly Empty State with Refresh Button & Informative Section */ +
+ {/* Ambient background glow */} +
+ + {/* Friendly Icon Badge */} +
+
+ +
+ + + + +
+ + {/* Friendly Heading */} +

+ {searchQuery ? 'No Matching Projects Found' : 'No Active Projects Assigned Yet'} +

+ + {/* Friendly Message */} +

+ {searchQuery + ? `We couldn't find any ongoing projects matching "${searchQuery}". Clear your search query to see your full workspace portfolio.` + : "Welcome to your Partner Workspace! Your funding organisation hasn't assigned any active project records to your profile just yet. If your funding award was recently approved or updated, click below to sync and retrieve your latest project schedule."} +

+ + {/* Refresh / Action Button */} +
+ {searchQuery ? ( + + ) : null} + + +
+ + {/* Elegantly placed informative text below all that */} +
+
+
+ + How Project Activation Works +
+ Post-Fund Lifecycle +
+ +
+
+
+ 1
-

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

-

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

Award Handover & Formal Agreement

+

+ Following sanction by the approval committee, your funding organisation registers the award. A legally binding grant agreement is automatically drafted for your review and digital execution.

+ +
+
+ 2 +
+
+

Milestones & Evidence Submission

+

+ Once the project is active, your work breakdown structure, deliverable due dates, and mandatory evidence requirements appear here so you can easily upload progress reports, invoices, and certificates. +

+
+
+ +
+
+ 3 +
+
+

Payment Requests & Controlled Tranches

+

+ Funds are released safely against verified milestone deliverables and satisfied conditions precedent. You can initiate drawdown requests directly from your project workspace. +

+
+
+ +
+
+ + Need status clarification? Contact your assigned Programme Officer. +
+ +
+
+
+
+ )} +
+
+ + {/* =============================================================== + RIGHT COLUMN: OVERVIEW (2x2 Grid) + INBOX (4 of 12 cols) + =============================================================== */} +
+ + {/* 1. OVERVIEW CARD (2x2 Grid with vertical divider) */} +
+

+ Overview +

+ +
+ {/* Subtle vertical divider between the 2 columns */} +
+ + {/* Left Column: Running Project & Completed Project */} +
+
+

+ {overviewMetrics.runningProjects} +

+

Running Project

+
+ +
+

+ {overviewMetrics.completedProjects} +

+

Completed Project

+
+
+ + {/* Right Column: Have to Pay & Total Payment */} +
+
+

+ {overviewMetrics.pendingPayment} +

+

Have to Pay

+
+ +
+

+ {overviewMetrics.totalPayment} +

+

Total Payment

+
+
+
+
+ + {/* 2. INBOX CARD (Matching Dribbble design: Inbox + badge, See All, and clean avatar list) */} +
+
+
+

Inbox

+ + {unreadCount > 0 ? unreadCount : 3} + +
+ +
+ + {/* List of Contacts & Messages */} +
+ {combinedInbox.map((item) => ( +
{ + const realMsg = inboxNotifications.find(x => String(x.id) === item.id); + if (realMsg) { + markAsRead(realMsg.id); + setSelectedNotification(realMsg); + } else { + // Demo item: open contact popup to reply + setContactTarget({ + organisation: partnerSummary.primaryFundingOrg, + projectTitle: item.name, + }); + setMessageSubject(`Re: Message from ${item.name}`); + setMessageBody(''); + } + }} + className="pt-3.5 first:pt-0 flex items-center gap-3.5 group cursor-pointer hover:bg-white/[0.02] p-1.5 rounded-xl transition-colors" + > +
+ {item.avatarInitials} +
+
+
+ + {item.name} + + {item.unread && ( + + )} +
+

+ {item.snippet} +

+
+
+ ))} +
+
+ +
+
+ ) : activeTab === 'messages' ? ( + /* ================================================================= + MESSAGES TAB + ================================================================= */ +
+
+
+

Messages & Communications

+

+ Direct communication with your funding programme officers. +

+
+ +
+ +
+ {inboxNotifications.length === 0 ? ( +
+ No new messages in your inbox yet. Messages sent by programme managers will be displayed here. +
+ ) : ( + 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 + + )} +
+

{stripMarkdown(msg.message)}

+
+
+ + {formatFullDate(msg.createdAt)} + +
+ ); + }) + )} +
+
+ ) : activeTab === 'contracts' ? ( + /* ================================================================= + CONTRACTS & ORDERS TAB + ================================================================= */ +
+
+

Contracts & Approved Grants

+

+ Review your approved funding awards and download or sign 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') && ( + + )}
- ); - })} -
- )} +
+ ); + }) + )} +
-
- )} -
+ ) : activeTab === 'analytics' ? ( + /* ================================================================= + ANALYTICS TAB + ================================================================= */ +
+
+

Portfolio Analytics

+

+ Performance metrics, milestone velocity, and financial drawdown burn rates. +

+
+ +
+
+
+ + +18% vs target +
+

92.4%

+

Milestone Completion Rate

+
+ +
+
+ + On Track +
+

{overviewMetrics.runningProjects} Active

+

Funded Projects in Execution

+
+ +
+
+ + Audited +
+

100%

+

Compliance & Evidence Health

+
+
+
+ ) : ( + /* ================================================================= + DOCUMENTS & EARNINGS TAB + ================================================================= */ +
+
+

Deliverables & Evidence Documents

+

+ Invoices, payment requests, and verified deliverable proofs submitted across 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 submit proof files. +

+
+ ) : ( +
+ {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 + MODAL 1: PROJECT WORKSPACE (From "View Details" Button) ===================================================================== */} {activeProjectModalItem && (
- {/* Modal Header */}
- + Project Workspace @@ -1597,7 +1771,6 @@ export function DashboardPage() {
- {/* Simple Step-by-Step Tabs */}
{( [ @@ -1614,7 +1787,7 @@ export function DashboardPage() { onClick={() => setProjectModalTab(t.id)} className={`px-4 py-2.5 text-xs font-semibold border-b-2 transition-colors whitespace-nowrap cursor-pointer ${ projectModalTab === t.id - ? 'border-emerald-400 text-emerald-300' + ? 'border-indigo-400 text-indigo-300' : 'border-transparent text-slate-400 hover:text-slate-200' }`} > @@ -1623,7 +1796,6 @@ export function DashboardPage() { ))}
- {/* Modal Body */}
{projectModalTab === 'milestones' && (
@@ -1647,7 +1819,7 @@ export function DashboardPage() { className={`h-7 w-7 rounded-full flex items-center justify-center text-xs font-bold ${ isVerified ? 'bg-emerald-500 text-slate-950' - : 'bg-slate-800 text-emerald-300 border border-emerald-500/30' + : 'bg-slate-800 text-indigo-300 border border-indigo-500/30' }`} > {isVerified ? : idx + 1} @@ -1675,7 +1847,7 @@ export function DashboardPage() { setSelectedMilestoneForUpload(String(m.id)); setProjectModalTab('evidence'); }} - className="px-3 py-1.5 rounded-lg bg-emerald-500/15 hover:bg-emerald-500/25 text-emerald-300 text-xs font-semibold transition-colors cursor-pointer" + className="px-3 py-1.5 rounded-lg bg-indigo-500/15 hover:bg-indigo-500/25 text-indigo-300 text-xs font-semibold transition-colors cursor-pointer" > Upload Proof @@ -1689,7 +1861,7 @@ export function DashboardPage() { {canAdd('projects_milestones') && (
-

+

Add a New Milestone

@@ -1711,7 +1883,7 @@ export function DashboardPage() { type="button" onClick={() => addMilestoneMutation.mutate()} disabled={addMilestoneMutation.isPending} - className="px-4 py-2 rounded-xl bg-emerald-500 hover:bg-emerald-400 text-slate-950 font-semibold text-xs cursor-pointer disabled:opacity-50" + className="px-4 py-2 rounded-xl bg-indigo-500 hover:bg-indigo-400 text-white font-semibold text-xs cursor-pointer disabled:opacity-50" > {addMilestoneMutation.isPending ? 'Saving...' : 'Add Milestone'} @@ -1722,9 +1894,8 @@ export function DashboardPage() { {projectModalTab === 'evidence' && (
- {/* Simple Upload Box */} -
-
+
+

Upload Supporting Document or Proof @@ -1751,17 +1922,18 @@ export function DashboardPage() { ))}

+
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" + className="w-full px-3 py-2 rounded-xl bg-slate-950 border border-white/10 text-xs text-slate-300 file:mr-3 file:py-1 file:px-3 file:rounded-lg file:border-0 file:text-xs file:font-semibold file:bg-indigo-500/20 file:text-indigo-300" /> -
-
- {/* 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. -

+
+

Request a Grant Payment Tranche

- + setPaymentAmount(e.target.value)} - placeholder="e.g. 50000" + placeholder="e.g. 150000" className="w-full px-3.5 py-2 rounded-xl bg-slate-950 border border-white/10 text-xs text-white" />
@@ -1859,7 +1985,7 @@ export function DashboardPage() { onChange={e => setPaymentMilestoneId(e.target.value)} className="w-full px-3.5 py-2 rounded-xl bg-slate-950 border border-white/10 text-xs text-white" > - + {activeProjectModalItem.milestones.map(m => (
- - {/* 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

-
+

Log Project Activity 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" + placeholder="Activity title" + className="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)" + placeholder="Cost incurred (optional)" className="px-3.5 py-2 rounded-xl bg-slate-950 border border-white/10 text-xs text-white" />