History
Your past contributions
- {/* Stats */}
+ {/* Stats Cards */}
{Object.entries(counts).map(([key, value]) => (
@@ -53,53 +269,140 @@ export default function History() {
))}
- {/* Filters */}
-
- {(['all', 'AVAILABLE', 'CLAIMED', 'DELIVERED'] as const).map((status) => (
+ {/* Tab Toggle — Charts tab only for NGOs */}
+ {isNGO && (
+
- ))}
-
+
+
+ )}
- {/* Table */}
-
- {loading ? (
-
Loading...
- ) : filtered.length === 0 ? (
-
No donations found
- ) : (
-
-
-
- | Item |
- Quantity |
- Status |
-
-
-
- {filtered.map((d) => (
-
- | {d.name} |
- {d.quantity} |
-
-
- {d.status}
-
- |
-
- ))}
-
-
- )}
-
+ {/* ── Charts View (NGO only) ── */}
+ {isNGO && tab === 'charts' ? (
+
+
+
+
+
Growth Reports
+
Monthly food intake summaries to support funding applications. Showing the last 6 months of activity.
+
+
+
+ {/* Chart 1 – Total food intake (bar) */}
+
+
+ {/* Chart 2 – Deliveries completed (line) */}
+
+
+ {/* Chart 3 – Claims made (bar) */}
+
+
+ {/* Summary Card for Funding */}
+
+
+
+ 6-Month Summary
+
+
+
+
{monthlyAll.reduce((s, d) => s + d.value, 0)}
+
Total Donations Received
+
+
+
{monthlyDelivered.reduce((s, d) => s + d.value, 0)}
+
Successful Deliveries
+
+
+
+ {monthlyAll.reduce((s, d) => s + d.value, 0) > 0
+ ? `${Math.round((monthlyDelivered.reduce((s, d) => s + d.value, 0) / monthlyAll.reduce((s, d) => s + d.value, 1)) * 100)}%`
+ : '—'}
+
+
Delivery Rate
+
+
+
+ 💡 Use this data to demonstrate impact in your funding applications. A high delivery rate signals operational efficiency to grant committees.
+
+
+
+ ) : (
+ /* ── Table View ── */
+ <>
+ {/* Filters */}
+
+ {(['all', 'AVAILABLE', 'CLAIMED', 'DELIVERED'] as const).map((status) => (
+
+ ))}
+
+
+
+ {loading ? (
+
Loading...
+ ) : filtered.length === 0 ? (
+
No donations found
+ ) : (
+
+
+
+ | Item |
+ Quantity |
+ Status |
+
+
+
+ {filtered.map((d) => (
+
+ | {d.name} |
+ {d.quantity} |
+
+
+ {d.status}
+
+ |
+
+ ))}
+
+
+ )}
+
+ >
+ )}
)
-}
+}
\ No newline at end of file
diff --git a/frontend/src/pages/dashboard/Profile.tsx b/frontend/src/pages/dashboard/Profile.tsx
index 6cf1738..e282199 100644
--- a/frontend/src/pages/dashboard/Profile.tsx
+++ b/frontend/src/pages/dashboard/Profile.tsx
@@ -1,6 +1,165 @@
-import { useEffect, useState } from 'react'
+import { useEffect, useState, useRef } from 'react'
import { getUserProfile, updateUserProfile, type User } from '../../services/api'
-import { User as UserIcon, Building, Phone, Mail, MapPin, Shield, Edit2, Check, Trophy, Star, AlertCircle, Loader2 } from 'lucide-react'
+import { User as UserIcon, Building, Phone, Mail, MapPin, Shield, Edit2, Check, Trophy, Star, AlertCircle, Loader2, Download, Award } from 'lucide-react'
+
+// ─── Certificate Modal (Donor Only) ──────────────────────────────────────────
+
+interface CertificateProps {
+ user: User
+ onClose: () => void
+}
+
+function CertificateModal({ user, onClose }: CertificateProps) {
+ const certRef = useRef
(null)
+
+ const today = new Date().toLocaleDateString('en-IN', {
+ day: 'numeric', month: 'long', year: 'numeric',
+ })
+ const donations = user.impactStats?.totalDonations ?? 0
+ const meals = user.impactStats?.mealsProvided ?? 0
+ const kg = user.impactStats?.kgSaved ?? 0
+
+ const handlePrint = () => {
+ const printContents = certRef.current?.innerHTML
+ if (!printContents) return
+ const pw = window.open('', '_blank', 'width=900,height=680')
+ if (!pw) return
+ pw.document.write(`
+ Certificate – ${user.organizationName || user.name}
+
+ `)
+ pw.document.close()
+ setTimeout(() => { pw.print(); pw.close() }, 500)
+ }
+
+ return (
+
+
+
+ {/* Toolbar */}
+
+
+
+ Certificate of Appreciation
+
+
+
+
+
+
+
+ {/* Certificate Preview */}
+
+
+ {/* Corner accents */}
+ {[['top-3 left-3', 'border-t-4 border-l-4 rounded-tl-lg'], ['top-3 right-3', 'border-t-4 border-r-4 rounded-tr-lg'], ['bottom-3 left-3', 'border-b-4 border-l-4 rounded-bl-lg'], ['bottom-3 right-3', 'border-b-4 border-r-4 rounded-br-lg']].map(([pos, style]) => (
+
+ ))}
+
+
🌾
+
Food Redistribution Platform
+
+
+ Certificate of Appreciation
+
+
+ In Recognition of Outstanding Generosity
+
+
+
+
+
+ This certificate is proudly presented to
+
+
+ {user.organizationName || user.name}
+
+
+ in acknowledgement of their compassionate food donations that have made a tangible difference in our community.
+
+
+ {/* Impact Stats */}
+
+ {[
+ { value: donations, label: 'Donations' },
+ { value: meals, label: 'Meals Provided' },
+ { value: `${kg} kg`, label: 'Food Saved' },
+ ].map(({ value, label }) => (
+
+ ))}
+
+
+ {/* Karma Badge */}
+
+
+ ⭐ {user.karmaPoints ?? 0} Karma Points · Level {user.level ?? 1} Contributor
+
+
+
+ {/* Footer */}
+
+
+
+
Platform Director
+
Food Redistribution Network
+
+
🏅
+
+
Date of Issue
+
{today}
+
+
+
+
+
+
+ )
+}
+
+// ─── Main Profile Component ───────────────────────────────────────────────────
export default function Profile() {
const [user, setUser] = useState(null)
@@ -8,68 +167,26 @@ export default function Profile() {
const [error, setError] = useState(null)
const [editing, setEditing] = useState(false)
const [saving, setSaving] = useState(false)
- const [formData, setFormData] = useState({
- name: '',
- phoneNumber: '',
- address: '',
- organizationName: '',
- })
+ const [showCertificate, setShowCertificate] = useState(false)
+ const [formData, setFormData] = useState({ name: '', phoneNumber: '', address: '', organizationName: '' })
- useEffect(() => {
- loadProfile()
- }, [])
+ useEffect(() => { loadProfile() }, [])
const loadProfile = async () => {
- console.log('🔍 Profile component: Starting to load profile...')
- setLoading(true)
- setError(null)
-
+ setLoading(true); setError(null)
try {
- // Check token first
const token = localStorage.getItem('token')
- if (!token) {
- throw new Error('No authentication token found')
- }
- console.log('✅ Token exists')
-
- // Call getUserProfile
- console.log('📞 Calling getUserProfile()...')
+ if (!token) throw new Error('No authentication token found')
const data = await getUserProfile()
- console.log('✅ getUserProfile returned:', data)
-
- if (!data) {
- throw new Error('getUserProfile returned null or undefined')
- }
-
- if (!data.id) {
- throw new Error('User data is missing id field')
- }
-
- console.log('✅ Profile loaded successfully!')
+ if (!data) throw new Error('getUserProfile returned null or undefined')
+ if (!data.id) throw new Error('User data is missing id field')
setUser(data)
- setFormData({
- name: data.name || '',
- phoneNumber: data.phoneNumber || data.phone || '',
- address: data.address || '',
- organizationName: data.organizationName || '',
- })
-
+ setFormData({ name: data.name || '', phoneNumber: data.phoneNumber || data.phone || '', address: data.address || '', organizationName: data.organizationName || '' })
} catch (err: any) {
- console.error('❌ Profile loading failed:', err)
- console.error('❌ Error type:', typeof err)
- console.error('❌ Error message:', err?.message)
- console.error('❌ Error response:', err?.response)
- console.error('❌ Full error object:', err)
-
- const errorMessage = err?.message || err?.response?.data?.message || 'Failed to load profile'
- setError(errorMessage)
-
- // If token is invalid, redirect to login
- if (err?.response?.status === 401 || errorMessage.includes('token')) {
- setTimeout(() => {
- localStorage.clear()
- window.location.href = '/login'
- }, 2000)
+ const msg = err?.message || err?.response?.data?.message || 'Failed to load profile'
+ setError(msg)
+ if (err?.response?.status === 401 || msg.includes('token')) {
+ setTimeout(() => { localStorage.clear(); window.location.href = '/login' }, 2000)
}
} finally {
setLoading(false)
@@ -80,95 +197,54 @@ export default function Profile() {
setSaving(true)
try {
const updated = await updateUserProfile(formData)
- setUser(updated)
- localStorage.setItem('user', JSON.stringify(updated))
- setEditing(false)
+ setUser(updated); localStorage.setItem('user', JSON.stringify(updated)); setEditing(false)
} catch (error: any) {
- console.error('Failed to update profile:', error)
alert('Failed to update profile: ' + (error.message || 'Unknown error'))
- } finally {
- setSaving(false)
- }
+ } finally { setSaving(false) }
}
- // Loading state
- if (loading) {
- return (
-
-
+ if (loading) return (
+
+
- )
- }
-
- // Error state
- if (error) {
- return (
-
-
-
-
Failed to Load Profile
-
{error}
-
-
-
-
-
-
+
+ )
-
-
- Debug Info:
- Token: {localStorage.getItem('token') ? '✓ Present' : '✗ Missing'}
- Error: {error}
-
-
+ if (error) return (
+
+
+
+
Failed to Load Profile
+
{error}
+
+
+
+
+
+
Debug Info:
Token: {localStorage.getItem('token') ? '✓ Present' : '✗ Missing'}
Error: {error}
- )
- }
+
+ )
- // No user state
- if (!user) {
- return (
-
-
-
-
No user data available
-
-
+ if (!user) return (
+
+
+
+
No user data available
+
- )
- }
+
+ )
- // Success! Show profile
const karmaPoints = user.karmaPoints || 0
const badges = user.badges || []
const level = user.level || 1
const nextLevelPoints = user.nextLevelPoints || 0
- const progressPercent = nextLevelPoints > 0
- ? Math.min(100, ((karmaPoints % 100) / nextLevelPoints) * 100)
- : 100
+ const progressPercent = nextLevelPoints > 0 ? Math.min(100, ((karmaPoints % 100) / nextLevelPoints) * 100) : 100
+ const isDonor = String(user.role).toLowerCase() === 'donor'
const getRoleBadge = () => {
const roleStr = String(user.role).toLowerCase()
@@ -179,12 +255,13 @@ export default function Profile() {
}
return badgeMap[roleStr as keyof typeof badgeMap] || badgeMap.donor
}
-
const badge = getRoleBadge()
return (
- {/* Header with Edit Button */}
+ {showCertificate &&
setShowCertificate(false)} />}
+
+ {/* Header */}
@@ -192,46 +269,61 @@ export default function Profile() {
{user.name?.[0]?.toUpperCase() || 'U'}
-
- {user.organizationName || user.name}
-
+
{user.organizationName || user.name}
-
- {badge.label}
-
+ {badge.label}
{user.isVerified && (
-
- Verified
+ Verified
)}
+
+ {isDonor && (
+
+ )}
+
+
+
+ Manage your account and view your impact
+
+ {/* Donor Certificate CTA */}
+ {isDonor && (
+
+
+
🏅
+
+
Certificate of Appreciation
+
Download your personalised impact certificate to share with your network or use in funding applications
+
+
-
Manage your account and view your impact
-
+ )}
{/* Karma Counter */}
@@ -242,11 +334,8 @@ export default function Profile() {
Karma Points
-
- Level {level} • {badge.label}
-
+
Level {level} • {badge.label}
-
{nextLevelPoints > 0 && (
@@ -254,10 +343,7 @@ export default function Profile() {
{nextLevelPoints} points to go
)}
@@ -266,10 +352,8 @@ export default function Profile() {
{/* Trophy Case */}
-
- Trophy Case
+ Trophy Case
-
{badges.length === 0 ? (
No badges earned yet
@@ -278,16 +362,9 @@ export default function Profile() {
) : (
{badges.map((badgeText, index) => (
-
-
- {badgeText.split(' ')[0]}
-
-
- {badgeText.split(' ').slice(1).join(' ')}
-
+
+
{badgeText.split(' ')[0]}
+
{badgeText.split(' ').slice(1).join(' ')}
))}
@@ -297,85 +374,29 @@ export default function Profile() {
{/* Account Information */}
Account Information
-
-
- {editing ? (
-
setFormData({ ...formData, name: e.target.value })}
- className="w-full bg-slate-950 border border-slate-800 rounded-lg px-4 py-2.5 text-white focus:border-emerald-500 focus:outline-none"
- />
- ) : (
-
{user.name}
- )}
+
+ {editing ?
setFormData({ ...formData, name: e.target.value })} className="w-full bg-slate-950 border border-slate-800 rounded-lg px-4 py-2.5 text-white focus:border-emerald-500 focus:outline-none" /> :
{user.name}
}
-
-
+
{user.email}
Email cannot be changed
-
-
- {editing ? (
-
setFormData({ ...formData, phoneNumber: e.target.value })}
- className="w-full bg-slate-950 border border-slate-800 rounded-lg px-4 py-2.5 text-white focus:border-emerald-500 focus:outline-none"
- />
- ) : (
-
{user.phoneNumber || user.phone || 'Not provided'}
- )}
+
+ {editing ?
setFormData({ ...formData, phoneNumber: e.target.value })} className="w-full bg-slate-950 border border-slate-800 rounded-lg px-4 py-2.5 text-white focus:border-emerald-500 focus:outline-none" /> :
{user.phoneNumber || user.phone || 'Not provided'}
}
-
{(String(user.role).toLowerCase() === 'donor' || String(user.role).toLowerCase() === 'ngo') && (
<>
-
- {editing ? (
-
setFormData({ ...formData, organizationName: e.target.value })}
- className="w-full bg-slate-950 border border-slate-800 rounded-lg px-4 py-2.5 text-white focus:border-emerald-500 focus:outline-none"
- />
- ) : (
-
{user.organizationName || 'Not provided'}
- )}
+
+ {editing ?
setFormData({ ...formData, organizationName: e.target.value })} className="w-full bg-slate-950 border border-slate-800 rounded-lg px-4 py-2.5 text-white focus:border-emerald-500 focus:outline-none" /> :
{user.organizationName || 'Not provided'}
}
-
-
- {editing ? (
-
>
)}
@@ -386,41 +407,12 @@ export default function Profile() {
📚 Badge Guide
-
-
🌱
-
-
Newcomer
-
Earn 50 karma points
+ {[{ e: '🌱', n: 'Newcomer', p: 50 }, { e: '🦸', n: 'Local Hero', p: 100 }, { e: '🏆', n: 'Champion', p: 250 }, { e: '⭐', n: 'Legend', p: 500 }, { e: '💫', n: 'Superhero', p: 1000 }].map(b => (
+
+
{b.e}
+
{b.n}
Earn {b.p} karma points
-
-
-
🦸
-
-
Local Hero
-
Earn 100 karma points
-
-
-
-
🏆
-
-
Champion
-
Earn 250 karma points
-
-
-
-
⭐
-
-
Legend
-
Earn 500 karma points
-
-
-
-
💫
-
-
Superhero
-
Earn 1000 karma points
-
-
+ ))}
@@ -430,17 +422,11 @@ export default function Profile() {
+50
-
-
Volunteer Delivery
-
Complete a food delivery as a volunteer
-
+
Volunteer Delivery
Complete a food delivery as a volunteer
+30
-
-
Donor Contribution
-
Your donated food is successfully delivered
-
+
Donor Contribution
Your donated food is successfully delivered
@@ -448,9 +434,7 @@ export default function Profile() {
{!user.isVerified && String(user.role).toLowerCase() === 'ngo' && (
⏳ Verification Pending
-
- Your NGO account is under review. You'll receive access once verified by our team.
-
+
Your NGO account is under review. You'll receive access once verified by our team.
)}