diff --git a/frontend/src/pages/dashboard/History.tsx b/frontend/src/pages/dashboard/History.tsx index bb10fb1..00eab6e 100644 --- a/frontend/src/pages/dashboard/History.tsx +++ b/frontend/src/pages/dashboard/History.tsx @@ -1,11 +1,222 @@ -import { useState, useEffect } from 'react' +import { useState, useEffect, useMemo } from 'react' import { getDonations } from '../../services/api' import type { Donation } from '../../services/api' +import { TrendingUp, BarChart2, Package } from 'lucide-react' + +// ─── Inline SVG Bar Chart ───────────────────────────────────────────────────── + +interface BarChartProps { + data: { label: string; value: number; color?: string }[] + height?: number + unit?: string + title: string + subtitle?: string + accentColor?: string +} + +function BarChart({ data, height = 180, unit = '', title, subtitle, accentColor = '#10b981' }: BarChartProps) { + const maxVal = Math.max(...data.map(d => d.value), 1) + const chartWidth = 560 + const chartHeight = height + const barAreaHeight = chartHeight - 32 // leave room for labels + const barCount = data.length + const barWidth = Math.floor((chartWidth - (barCount - 1) * 8) / barCount) + const gap = 8 + + return ( +
+
+

+ + {title} +

+ {subtitle &&

{subtitle}

} +
+
+ + {/* Grid lines */} + {[0.25, 0.5, 0.75, 1].map((frac) => { + const y = barAreaHeight - barAreaHeight * frac + return ( + + + + {Math.round(maxVal * frac)}{unit} + + + ) + })} + + {/* Bars */} + {data.map((d, i) => { + const barH = d.value > 0 ? Math.max(4, (d.value / maxVal) * barAreaHeight) : 2 + const x = i * (barWidth + gap) + const y = barAreaHeight - barH + const barColor = d.color || accentColor + + return ( + + {/* Bar shadow */} + + {/* Bar */} + + {/* Value label */} + {d.value > 0 && ( + + {d.value}{unit} + + )} + {/* Month label */} + + {d.label} + + + ) + })} + +
+
+ ) +} + +// ─── Line/Area Trend Chart ──────────────────────────────────────────────────── + +interface LineChartProps { + data: { label: string; value: number }[] + title: string + subtitle?: string + accentColor?: string +} + +function LineChart({ data, title, subtitle, accentColor = '#6366f1' }: LineChartProps) { + const width = 560 + const height = 160 + const padTop = 20 + const padBot = 28 + const padLeft = 30 + const padRight = 10 + const innerW = width - padLeft - padRight + const innerH = height - padTop - padBot + + const maxVal = Math.max(...data.map(d => d.value), 1) + const pts = data.map((d, i) => { + const x = padLeft + (i / Math.max(data.length - 1, 1)) * innerW + const y = padTop + (1 - d.value / maxVal) * innerH + return { x, y, ...d } + }) + + const polyline = pts.map(p => `${p.x},${p.y}`).join(' ') + // Close the area path + const areaPath = pts.length >= 2 + ? `M${pts[0].x},${padTop + innerH} L${pts.map(p => `${p.x},${p.y}`).join(' L')} L${pts[pts.length - 1].x},${padTop + innerH} Z` + : '' + + return ( +
+
+

+ + {title} +

+ {subtitle &&

{subtitle}

} +
+
+ + + + + + + + {/* Grid */} + {[0.25, 0.5, 0.75, 1].map(frac => { + const y = padTop + (1 - frac) * innerH + return ( + + + + {Math.round(maxVal * frac)} + + + ) + })} + {/* Area */} + {areaPath && } + {/* Line */} + {pts.length >= 2 && ( + + )} + {/* Dots + labels */} + {pts.map((p, i) => ( + + + {p.label} + {p.value > 0 && ( + {p.value} + )} + + ))} + +
+
+ ) +} + +// ─── Monthly Data Helpers ───────────────────────────────────────────────────── + +const MONTH_ABBR = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'] +const LAST_6_MONTHS = Array.from({ length: 6 }, (_, i) => { + const d = new Date() + d.setMonth(d.getMonth() - 5 + i) + return { month: d.getMonth(), year: d.getFullYear(), label: MONTH_ABBR[d.getMonth()] } +}) + +function aggregateByMonth(donations: Donation[], statusFilter?: string) { + return LAST_6_MONTHS.map(({ month, year, label }) => { + const count = donations.filter(d => { + const dt = new Date(d.createdAt) + return dt.getMonth() === month && dt.getFullYear() === year && (!statusFilter || d.status === statusFilter) + }).length + return { label, value: count } + }) +} + +// ─── Main History Component ─────────────────────────────────────────────────── + +const userRole = (): string => { + try { + const u = JSON.parse(localStorage.getItem('user') || '{}') + return String(u.role || '').toLowerCase() + } catch { return '' } +} export default function History() { const [donations, setDonations] = useState([]) const [filter, setFilter] = useState<'all' | 'AVAILABLE' | 'CLAIMED' | 'DELIVERED'>('all') const [loading, setLoading] = useState(true) + const [tab, setTab] = useState<'table' | 'charts'>('table') + + const role = userRole() + const isNGO = role === 'ngo' useEffect(() => { const load = async () => { @@ -38,12 +249,17 @@ export default function History() { DELIVERED: donations.filter(d => d.status === 'DELIVERED').length, } + // Chart data + const monthlyAll = useMemo(() => aggregateByMonth(donations), [donations]) + const monthlyDelivered = useMemo(() => aggregateByMonth(donations, 'DELIVERED'), [donations]) + const monthlyClaimed = useMemo(() => aggregateByMonth(donations, 'CLAIMED'), [donations]) + return (

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
- ) : ( - - - - - - - - - - {filtered.map((d) => ( - - - - - - ))} - -
ItemQuantityStatus
{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
+ ) : ( + + + + + + + + + + {filtered.map((d) => ( + + + + + + ))} + +
ItemQuantityStatus
{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} + +
+
+
+ ${printContents} +
`) + 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 }) => ( +
+
{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 ( -
-
- -

Loading profile...

-
+ if (loading) return ( +
+
+ +

Loading profile...

- ) - } - - // 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 ? ( -