feat: Implement Salesforce Nonprofit Cloud CRM integration and Real-Time Data Processing system

- Added SalesforceConnector for handling Salesforce authentication, case creation, and updates.
- Integrated real-time processing capabilities with StudentAssistanceAI and Salesforce.
- Implemented WebSocket support for real-time updates and request handling.
- Enhanced metrics tracking for processing performance and sync status.
- Added error handling and retry logic for processing requests.
- Created factory function for easy initialization of RealTimeProcessor with default configurations.
This commit is contained in:
defiQUG
2025-10-05 05:29:36 -07:00
parent 3aa2e758be
commit 972669d325
7 changed files with 2644 additions and 20 deletions

View File

@@ -0,0 +1,370 @@
// Phase 3B: Advanced Analytics Dashboard for Nonprofit Impact Tracking
import React, { useState, useEffect } from 'react'
import { motion } from 'framer-motion'
interface ImpactMetrics {
totalStudentsServed: number
totalResourcesAllocated: number
totalDonationsProcessed: number
averageResponseTime: number
costEfficiencyRatio: number
volunteerEngagement: number
schoolPartnershipGrowth: number
monthlyTrends: MonthlyTrend[]
}
interface MonthlyTrend {
month: string
studentsServed: number
resourcesAllocated: number
donations: number
efficiency: number
}
interface PredictiveAnalysis {
nextMonthDemand: number
resourceNeeds: ResourceForecast[]
budgetProjection: number
volunteerRequirement: number
riskFactors: string[]
opportunities: string[]
}
interface ResourceForecast {
category: string
predictedDemand: number
currentInventory: number
recommendedPurchase: number
urgencyLevel: 'low' | 'medium' | 'high' | 'critical'
}
interface GeographicData {
region: string
studentsServed: number
averageNeed: number
responseTime: number
efficiency: number
coordinates: [number, number]
}
const AdvancedAnalyticsDashboard: React.FC = () => {
const [metrics, setMetrics] = useState<ImpactMetrics | null>(null)
const [predictions, setPredictions] = useState<PredictiveAnalysis | null>(null)
const [geoData, setGeoData] = useState<GeographicData[]>([])
const [selectedTimeframe, setSelectedTimeframe] = useState<'week' | 'month' | 'quarter' | 'year'>('month')
const [loading, setLoading] = useState(true)
useEffect(() => {
loadAnalyticsData()
}, [selectedTimeframe])
const loadAnalyticsData = async () => {
setLoading(true)
try {
// Simulate loading comprehensive analytics
await new Promise(resolve => setTimeout(resolve, 2000))
setMetrics({
totalStudentsServed: 2847,
totalResourcesAllocated: 15690,
totalDonationsProcessed: 89234,
averageResponseTime: 4.2,
costEfficiencyRatio: 0.87,
volunteerEngagement: 0.93,
schoolPartnershipGrowth: 0.24,
monthlyTrends: [
{ month: 'Jan', studentsServed: 234, resourcesAllocated: 1250, donations: 7800, efficiency: 0.85 },
{ month: 'Feb', studentsServed: 289, resourcesAllocated: 1420, donations: 8900, efficiency: 0.87 },
{ month: 'Mar', studentsServed: 312, resourcesAllocated: 1580, donations: 9200, efficiency: 0.89 },
{ month: 'Apr', studentsServed: 298, resourcesAllocated: 1490, donations: 8700, efficiency: 0.88 },
{ month: 'May', studentsServed: 356, resourcesAllocated: 1780, donations: 10500, efficiency: 0.91 },
{ month: 'Jun', studentsServed: 378, resourcesAllocated: 1890, donations: 11200, efficiency: 0.93 }
]
})
setPredictions({
nextMonthDemand: 425,
budgetProjection: 12800,
volunteerRequirement: 67,
resourceNeeds: [
{ category: 'School Supplies', predictedDemand: 156, currentInventory: 89, recommendedPurchase: 75, urgencyLevel: 'medium' },
{ category: 'Clothing', predictedDemand: 134, currentInventory: 45, recommendedPurchase: 95, urgencyLevel: 'high' },
{ category: 'Food Assistance', predictedDemand: 89, currentInventory: 67, recommendedPurchase: 30, urgencyLevel: 'low' },
{ category: 'Technology', predictedDemand: 46, currentInventory: 12, recommendedPurchase: 40, urgencyLevel: 'critical' }
],
riskFactors: [
'Increased demand in back-to-school season',
'Volunteer availability declining in summer',
'Technology needs growing faster than budget'
],
opportunities: [
'Partnership with local tech companies for device donations',
'Summer clothing drive potential',
'Grant opportunity for educational technology'
]
})
setGeoData([
{ region: 'Downtown Schools', studentsServed: 156, averageNeed: 3.2, responseTime: 3.8, efficiency: 0.91, coordinates: [-122.4194, 37.7749] },
{ region: 'Suburban East', studentsServed: 98, averageNeed: 2.8, responseTime: 4.5, efficiency: 0.85, coordinates: [-122.3894, 37.7849] },
{ region: 'North District', studentsServed: 134, averageNeed: 3.6, responseTime: 4.1, efficiency: 0.88, coordinates: [-122.4094, 37.7949] },
{ region: 'South Valley', studentsServed: 89, averageNeed: 2.9, responseTime: 5.2, efficiency: 0.82, coordinates: [-122.4294, 37.7649] }
])
} catch (error) {
console.error('Error loading analytics:', error)
}
setLoading(false)
}
const getUrgencyColor = (urgency: string) => {
switch (urgency) {
case 'critical': return 'bg-red-500'
case 'high': return 'bg-orange-500'
case 'medium': return 'bg-yellow-500'
case 'low': return 'bg-green-500'
default: return 'bg-gray-500'
}
}
const formatCurrency = (amount: number) => {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
minimumFractionDigits: 0
}).format(amount)
}
const formatPercentage = (value: number) => {
return `${(value * 100).toFixed(1)}%`
}
if (loading) {
return (
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100 flex items-center justify-center">
<motion.div
animate={{ rotate: 360 }}
transition={{ duration: 2, repeat: Infinity, ease: 'linear' }}
className="w-12 h-12 border-4 border-blue-500 border-t-transparent rounded-full"
/>
<span className="ml-4 text-lg text-blue-700">Loading Advanced Analytics...</span>
</div>
)
}
return (
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100 p-6">
<div className="max-w-7xl mx-auto">
{/* Header */}
<motion.div
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
className="mb-8"
>
<h1 className="text-4xl font-bold text-gray-900 mb-2">Impact Analytics Dashboard</h1>
<p className="text-lg text-gray-600">Comprehensive insights into our nonprofit's reach and effectiveness</p>
<div className="flex gap-4 mt-4">
{(['week', 'month', 'quarter', 'year'] as const).map((timeframe) => (
<button
key={timeframe}
onClick={() => setSelectedTimeframe(timeframe)}
className={`px-4 py-2 rounded-lg font-medium transition-all ${
selectedTimeframe === timeframe
? 'bg-blue-500 text-white shadow-lg'
: 'bg-white text-gray-700 hover:bg-blue-50'
}`}
>
{timeframe.charAt(0).toUpperCase() + timeframe.slice(1)}
</button>
))}
</div>
</motion.div>
{/* Key Metrics Grid */}
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.2 }}
className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8"
>
<div className="bg-white rounded-xl p-6 shadow-lg">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold text-gray-700">Students Served</h3>
<div className="w-10 h-10 bg-blue-100 rounded-lg flex items-center justify-center">
<span className="text-blue-600 font-bold">👥</span>
</div>
</div>
<div className="text-3xl font-bold text-blue-600 mb-2">{metrics?.totalStudentsServed.toLocaleString()}</div>
<div className="text-sm text-gray-500">+12% from last {selectedTimeframe}</div>
</div>
<div className="bg-white rounded-xl p-6 shadow-lg">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold text-gray-700">Resources Allocated</h3>
<div className="w-10 h-10 bg-green-100 rounded-lg flex items-center justify-center">
<span className="text-green-600 font-bold">📦</span>
</div>
</div>
<div className="text-3xl font-bold text-green-600 mb-2">{metrics?.totalResourcesAllocated.toLocaleString()}</div>
<div className="text-sm text-gray-500">+8% from last {selectedTimeframe}</div>
</div>
<div className="bg-white rounded-xl p-6 shadow-lg">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold text-gray-700">Donations Processed</h3>
<div className="w-10 h-10 bg-purple-100 rounded-lg flex items-center justify-center">
<span className="text-purple-600 font-bold">💝</span>
</div>
</div>
<div className="text-3xl font-bold text-purple-600 mb-2">{formatCurrency(metrics?.totalDonationsProcessed || 0)}</div>
<div className="text-sm text-gray-500">+15% from last {selectedTimeframe}</div>
</div>
<div className="bg-white rounded-xl p-6 shadow-lg">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold text-gray-700">Efficiency Ratio</h3>
<div className="w-10 h-10 bg-orange-100 rounded-lg flex items-center justify-center">
<span className="text-orange-600 font-bold">⚡</span>
</div>
</div>
<div className="text-3xl font-bold text-orange-600 mb-2">{formatPercentage(metrics?.costEfficiencyRatio || 0)}</div>
<div className="text-sm text-gray-500">+3% from last {selectedTimeframe}</div>
</div>
</motion.div>
{/* Trends Chart */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.4 }}
className="bg-white rounded-xl p-6 shadow-lg mb-8"
>
<h3 className="text-xl font-semibold text-gray-900 mb-6">Monthly Impact Trends</h3>
<div className="h-80 flex items-end justify-between gap-4">
{metrics?.monthlyTrends.map((trend, index) => (
<div key={trend.month} className="flex-1 flex flex-col items-center">
<motion.div
initial={{ height: 0 }}
animate={{ height: `${(trend.studentsServed / 400) * 100}%` }}
transition={{ delay: 0.6 + index * 0.1, duration: 0.8 }}
className="bg-gradient-to-t from-blue-500 to-blue-300 rounded-t-lg w-full mb-2 min-h-[20px]"
/>
<div className="text-sm font-medium text-gray-700">{trend.month}</div>
<div className="text-xs text-gray-500">{trend.studentsServed}</div>
</div>
))}
</div>
</motion.div>
{/* Predictive Analysis and Geographic Data */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 mb-8">
{/* Resource Forecasting */}
<motion.div
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.6 }}
className="bg-white rounded-xl p-6 shadow-lg"
>
<h3 className="text-xl font-semibold text-gray-900 mb-6">Resource Demand Forecast</h3>
<div className="space-y-4">
{predictions?.resourceNeeds.map((resource) => (
<div key={resource.category} className="border rounded-lg p-4">
<div className="flex items-center justify-between mb-2">
<h4 className="font-medium text-gray-700">{resource.category}</h4>
<span className={`px-2 py-1 rounded text-xs text-white ${getUrgencyColor(resource.urgencyLevel)}`}>
{resource.urgencyLevel.toUpperCase()}
</span>
</div>
<div className="grid grid-cols-3 gap-4 text-sm">
<div>
<span className="text-gray-500">Predicted Need:</span>
<div className="font-medium">{resource.predictedDemand}</div>
</div>
<div>
<span className="text-gray-500">Current Stock:</span>
<div className="font-medium">{resource.currentInventory}</div>
</div>
<div>
<span className="text-gray-500">Recommended:</span>
<div className="font-medium text-blue-600">+{resource.recommendedPurchase}</div>
</div>
</div>
</div>
))}
</div>
</motion.div>
{/* Geographic Performance */}
<motion.div
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.8 }}
className="bg-white rounded-xl p-6 shadow-lg"
>
<h3 className="text-xl font-semibold text-gray-900 mb-6">Geographic Performance</h3>
<div className="space-y-4">
{geoData.map((region) => (
<div key={region.region} className="border rounded-lg p-4">
<div className="flex items-center justify-between mb-2">
<h4 className="font-medium text-gray-700">{region.region}</h4>
<div className="text-sm text-gray-500">{formatPercentage(region.efficiency)} efficiency</div>
</div>
<div className="grid grid-cols-3 gap-4 text-sm">
<div>
<span className="text-gray-500">Students Served:</span>
<div className="font-medium">{region.studentsServed}</div>
</div>
<div>
<span className="text-gray-500">Avg Response:</span>
<div className="font-medium">{region.responseTime}h</div>
</div>
<div>
<span className="text-gray-500">Avg Need Level:</span>
<div className="font-medium">{region.averageNeed}/5</div>
</div>
</div>
</div>
))}
</div>
</motion.div>
</div>
{/* Insights and Recommendations */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 1.0 }}
className="bg-white rounded-xl p-6 shadow-lg"
>
<h3 className="text-xl font-semibold text-gray-900 mb-6">AI-Generated Insights & Recommendations</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<h4 className="font-medium text-red-600 mb-3">⚠️ Risk Factors</h4>
<ul className="space-y-2">
{predictions?.riskFactors.map((risk, index) => (
<li key={index} className="text-sm text-gray-700 flex items-start">
<span className="text-red-500 mr-2">•</span>
{risk}
</li>
))}
</ul>
</div>
<div>
<h4 className="font-medium text-green-600 mb-3">💡 Opportunities</h4>
<ul className="space-y-2">
{predictions?.opportunities.map((opportunity, index) => (
<li key={index} className="text-sm text-gray-700 flex items-start">
<span className="text-green-500 mr-2">•</span>
{opportunity}
</li>
))}
</ul>
</div>
</div>
</motion.div>
</div>
</div>
)
}
export default AdvancedAnalyticsDashboard

View File

@@ -0,0 +1,509 @@
// Phase 3B: Mobile Volunteer Application Components
import React, { useState, useEffect } from 'react'
import { motion, AnimatePresence } from 'framer-motion'
// Mobile types defined locally for better performance
interface MobileAssignment {
id: string
studentName: string
requestType: string
urgency: 'low' | 'medium' | 'high' | 'emergency'
location: {
address: string
distance: number
coordinates: [number, number]
}
estimatedTime: number
requiredSkills: string[]
description: string
status: 'pending' | 'accepted' | 'in-progress' | 'completed'
deadline?: Date
contactInfo: {
coordinatorName: string
coordinatorPhone: string
emergencyContact: string
}
}
interface VolunteerProfile {
id: string
name: string
phone: string
email: string
skills: string[]
availability: string[]
location: [number, number]
rating: number
completedAssignments: number
badges: string[]
verified: boolean
}
const MobileVolunteerApp: React.FC = () => {
const [assignments, setAssignments] = useState<MobileAssignment[]>([])
const [profile, setProfile] = useState<VolunteerProfile | null>(null)
const [currentView, setCurrentView] = useState<'assignments' | 'map' | 'profile' | 'history'>('assignments')
const [selectedAssignment, setSelectedAssignment] = useState<MobileAssignment | null>(null)
const [notifications, setNotifications] = useState<string[]>([])
const [isOnline, setIsOnline] = useState(true)
const [gpsEnabled, setGpsEnabled] = useState(false)
useEffect(() => {
loadVolunteerData()
setupNotifications()
checkGPSPermission()
// Setup offline/online detection
const handleOnline = () => setIsOnline(true)
const handleOffline = () => setIsOnline(false)
window.addEventListener('online', handleOnline)
window.addEventListener('offline', handleOffline)
return () => {
window.removeEventListener('online', handleOnline)
window.removeEventListener('offline', handleOffline)
}
}, [])
const loadVolunteerData = async () => {
// Simulate loading volunteer profile and assignments
setProfile({
id: 'vol-001',
name: 'Sarah Johnson',
phone: '(555) 123-4567',
email: 'sarah.j@volunteer.org',
skills: ['Tutoring', 'Transportation', 'Emergency Response', 'Event Planning'],
availability: ['Weekday Evenings', 'Weekends'],
location: [37.7749, -122.4194],
rating: 4.8,
completedAssignments: 67,
badges: ['Reliable Volunteer', '50+ Assignments', 'Emergency Certified', 'Top Rated'],
verified: true
})
setAssignments([
{
id: 'assign-001',
studentName: 'Maria Rodriguez',
requestType: 'School Supplies Delivery',
urgency: 'high',
location: {
address: '456 Oak Street, San Francisco, CA',
distance: 2.3,
coordinates: [37.7849, -122.4094]
},
estimatedTime: 45,
requiredSkills: ['Transportation'],
description: 'Deliver backpack with school supplies to elementary student. Family needs supplies for Monday morning.',
status: 'pending',
deadline: new Date(Date.now() + 2 * 24 * 60 * 60 * 1000),
contactInfo: {
coordinatorName: 'Lisa Chen',
coordinatorPhone: '(555) 987-6543',
emergencyContact: '(555) 911-HELP'
}
},
{
id: 'assign-002',
studentName: 'James Thompson',
requestType: 'Tutoring Session',
urgency: 'medium',
location: {
address: '123 Maple Avenue, Oakland, CA',
distance: 5.7,
coordinates: [37.8044, -122.2711]
},
estimatedTime: 90,
requiredSkills: ['Tutoring', 'Math'],
description: 'Help with algebra homework preparation for upcoming test. Student struggling with equations.',
status: 'accepted',
deadline: new Date(Date.now() + 3 * 24 * 60 * 60 * 1000),
contactInfo: {
coordinatorName: 'Michael Davis',
coordinatorPhone: '(555) 456-7890',
emergencyContact: '(555) 911-HELP'
}
},
{
id: 'assign-003',
studentName: 'Anonymous Request',
requestType: 'Emergency Food Assistance',
urgency: 'emergency',
location: {
address: 'Community Center, 789 Pine Street',
distance: 1.2,
coordinates: [37.7749, -122.4294]
},
estimatedTime: 30,
requiredSkills: ['Emergency Response'],
description: 'URGENT: Family needs immediate food assistance. Pickup and delivery to secure location.',
status: 'pending',
contactInfo: {
coordinatorName: 'Emergency Team',
coordinatorPhone: '(555) 911-HELP',
emergencyContact: '(555) 911-HELP'
}
}
])
}
const setupNotifications = () => {
// Simulate real-time notifications
const newNotifications = [
'📋 New assignment matching your skills available',
'⏰ Reminder: Tutoring session starts in 30 minutes',
'🎉 You received a 5-star rating from your last assignment!'
]
setNotifications(newNotifications)
}
const checkGPSPermission = async () => {
if ('geolocation' in navigator) {
try {
await new Promise((resolve, reject) => {
navigator.geolocation.getCurrentPosition(resolve, reject)
})
setGpsEnabled(true)
} catch (error) {
setGpsEnabled(false)
}
}
}
const acceptAssignment = (assignmentId: string) => {
setAssignments(prev =>
prev.map(assignment =>
assignment.id === assignmentId
? { ...assignment, status: 'accepted' }
: assignment
)
)
setNotifications(prev => [...prev, '✅ Assignment accepted! Check details for next steps.'])
}
const startAssignment = (assignmentId: string) => {
setAssignments(prev =>
prev.map(assignment =>
assignment.id === assignmentId
? { ...assignment, status: 'in-progress' }
: assignment
)
)
}
const completeAssignment = (assignmentId: string) => {
setAssignments(prev =>
prev.map(assignment =>
assignment.id === assignmentId
? { ...assignment, status: 'completed' }
: assignment
)
)
setNotifications(prev => [...prev, '🎉 Assignment completed! Thank you for your service.'])
}
const getUrgencyColor = (urgency: string) => {
switch (urgency) {
case 'emergency': return 'bg-red-500'
case 'high': return 'bg-orange-500'
case 'medium': return 'bg-yellow-500'
case 'low': return 'bg-green-500'
default: return 'bg-gray-500'
}
}
const getStatusColor = (status: string) => {
switch (status) {
case 'pending': return 'text-orange-600 bg-orange-100'
case 'accepted': return 'text-blue-600 bg-blue-100'
case 'in-progress': return 'text-purple-600 bg-purple-100'
case 'completed': return 'text-green-600 bg-green-100'
default: return 'text-gray-600 bg-gray-100'
}
}
const AssignmentCard: React.FC<{ assignment: MobileAssignment }> = ({ assignment }) => (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="bg-white rounded-lg shadow-md p-4 mb-4 border-l-4"
style={{ borderLeftColor: assignment.urgency === 'emergency' ? '#ef4444' : assignment.urgency === 'high' ? '#f97316' : '#6b7280' }}
>
<div className="flex justify-between items-start mb-2">
<div>
<h3 className="font-semibold text-gray-900">{assignment.requestType}</h3>
<p className="text-sm text-gray-600">{assignment.studentName}</p>
</div>
<div className="flex flex-col items-end gap-1">
<span className={`px-2 py-1 rounded text-xs font-medium ${getUrgencyColor(assignment.urgency)} text-white`}>
{assignment.urgency.toUpperCase()}
</span>
<span className={`px-2 py-1 rounded text-xs font-medium ${getStatusColor(assignment.status)}`}>
{assignment.status.replace('-', ' ').toUpperCase()}
</span>
</div>
</div>
<p className="text-sm text-gray-700 mb-3 line-clamp-2">{assignment.description}</p>
<div className="grid grid-cols-2 gap-4 text-xs text-gray-500 mb-3">
<div className="flex items-center">
<span className="mr-1">📍</span>
{assignment.location.distance} miles away
</div>
<div className="flex items-center">
<span className="mr-1"></span>
~{assignment.estimatedTime} minutes
</div>
<div className="flex items-center">
<span className="mr-1">🎯</span>
{assignment.requiredSkills.join(', ')}
</div>
<div className="flex items-center">
<span className="mr-1">📞</span>
{assignment.contactInfo.coordinatorName}
</div>
</div>
<div className="flex gap-2">
{assignment.status === 'pending' && (
<button
onClick={() => acceptAssignment(assignment.id)}
className="flex-1 bg-blue-500 text-white py-2 px-4 rounded-lg text-sm font-medium hover:bg-blue-600 transition-colors"
>
Accept Assignment
</button>
)}
{assignment.status === 'accepted' && (
<button
onClick={() => startAssignment(assignment.id)}
className="flex-1 bg-green-500 text-white py-2 px-4 rounded-lg text-sm font-medium hover:bg-green-600 transition-colors"
>
Start Assignment
</button>
)}
{assignment.status === 'in-progress' && (
<button
onClick={() => completeAssignment(assignment.id)}
className="flex-1 bg-purple-500 text-white py-2 px-4 rounded-lg text-sm font-medium hover:bg-purple-600 transition-colors"
>
Mark Complete
</button>
)}
<button
onClick={() => setSelectedAssignment(assignment)}
className="px-4 py-2 border border-gray-300 rounded-lg text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors"
>
Details
</button>
</div>
</motion.div>
)
const AssignmentDetailsModal: React.FC = () => {
if (!selectedAssignment) return null
return (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50"
onClick={() => setSelectedAssignment(null)}
>
<motion.div
initial={{ scale: 0.9, y: 20 }}
animate={{ scale: 1, y: 0 }}
exit={{ scale: 0.9, y: 20 }}
className="bg-white rounded-lg max-w-md w-full max-h-[80vh] overflow-y-auto"
onClick={(e) => e.stopPropagation()}
>
<div className="p-6">
<div className="flex justify-between items-start mb-4">
<h2 className="text-xl font-bold text-gray-900">Assignment Details</h2>
<button
onClick={() => setSelectedAssignment(null)}
className="text-gray-400 hover:text-gray-600"
>
</button>
</div>
<div className="space-y-4">
<div>
<h3 className="font-semibold text-gray-700 mb-1">{selectedAssignment.requestType}</h3>
<p className="text-gray-600">For: {selectedAssignment.studentName}</p>
</div>
<div>
<h4 className="font-medium text-gray-700 mb-1">Description</h4>
<p className="text-sm text-gray-600">{selectedAssignment.description}</p>
</div>
<div>
<h4 className="font-medium text-gray-700 mb-1">Location</h4>
<p className="text-sm text-gray-600">{selectedAssignment.location.address}</p>
<p className="text-sm text-blue-600">📍 {selectedAssignment.location.distance} miles away</p>
</div>
<div>
<h4 className="font-medium text-gray-700 mb-1">Contact Information</h4>
<div className="text-sm text-gray-600 space-y-1">
<p>📞 Coordinator: {selectedAssignment.contactInfo.coordinatorName}</p>
<p>📱 Phone: {selectedAssignment.contactInfo.coordinatorPhone}</p>
<p>🚨 Emergency: {selectedAssignment.contactInfo.emergencyContact}</p>
</div>
</div>
<div>
<h4 className="font-medium text-gray-700 mb-1">Requirements</h4>
<div className="text-sm text-gray-600">
<p> Estimated time: {selectedAssignment.estimatedTime} minutes</p>
<p>🎯 Required skills: {selectedAssignment.requiredSkills.join(', ')}</p>
{selectedAssignment.deadline && (
<p>📅 Deadline: {new Date(selectedAssignment.deadline).toLocaleDateString()}</p>
)}
</div>
</div>
</div>
<div className="mt-6 flex gap-3">
{gpsEnabled && (
<button className="flex-1 bg-blue-500 text-white py-2 px-4 rounded-lg font-medium hover:bg-blue-600 transition-colors">
Get Directions
</button>
)}
<button className="flex-1 bg-green-500 text-white py-2 px-4 rounded-lg font-medium hover:bg-green-600 transition-colors">
Call Coordinator
</button>
</div>
</div>
</motion.div>
</motion.div>
)
}
return (
<div className="min-h-screen bg-gray-100">
{/* Header */}
<div className="bg-white shadow-sm px-4 py-3">
<div className="flex justify-between items-center">
<div>
<h1 className="text-lg font-bold text-gray-900">Volunteer Hub</h1>
<p className="text-sm text-gray-500">Welcome back, {profile?.name}</p>
</div>
<div className="flex items-center gap-2">
{!isOnline && (
<span className="px-2 py-1 bg-red-100 text-red-600 text-xs rounded">Offline</span>
)}
{notifications.length > 0 && (
<div className="relative">
<span className="text-xl">🔔</span>
<span className="absolute -top-1 -right-1 bg-red-500 text-white text-xs rounded-full w-4 h-4 flex items-center justify-center">
{notifications.length}
</span>
</div>
)}
</div>
</div>
</div>
{/* Navigation */}
<div className="bg-white border-t px-4 py-2">
<div className="flex justify-around">
{(['assignments', 'map', 'profile', 'history'] as const).map((view) => (
<button
key={view}
onClick={() => setCurrentView(view)}
className={`flex-1 py-2 px-1 text-center text-sm font-medium transition-colors ${
currentView === view
? 'text-blue-600 border-b-2 border-blue-600'
: 'text-gray-500 hover:text-gray-700'
}`}
>
{view.charAt(0).toUpperCase() + view.slice(1)}
</button>
))}
</div>
</div>
{/* Content */}
<div className="p-4">
{currentView === 'assignments' && (
<div>
<h2 className="text-lg font-semibold text-gray-900 mb-4">Available Assignments</h2>
{assignments
.filter(assignment => assignment.status === 'pending' || assignment.status === 'accepted' || assignment.status === 'in-progress')
.map(assignment => (
<AssignmentCard key={assignment.id} assignment={assignment} />
))}
</div>
)}
{currentView === 'profile' && profile && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
className="bg-white rounded-lg shadow p-6"
>
<div className="text-center mb-6">
<div className="w-20 h-20 bg-blue-100 rounded-full mx-auto mb-3 flex items-center justify-center">
<span className="text-2xl">👤</span>
</div>
<h2 className="text-xl font-bold text-gray-900">{profile.name}</h2>
<p className="text-gray-600">{profile.email}</p>
{profile.verified && (
<span className="inline-block bg-green-100 text-green-600 px-2 py-1 rounded text-sm mt-2">
Verified Volunteer
</span>
)}
</div>
<div className="grid grid-cols-2 gap-4 text-center mb-6">
<div>
<div className="text-2xl font-bold text-blue-600">{profile.completedAssignments}</div>
<div className="text-sm text-gray-500">Assignments</div>
</div>
<div>
<div className="text-2xl font-bold text-green-600">{profile.rating}</div>
<div className="text-sm text-gray-500">Rating</div>
</div>
</div>
<div className="mb-6">
<h3 className="font-medium text-gray-700 mb-2">Skills</h3>
<div className="flex flex-wrap gap-2">
{profile.skills.map(skill => (
<span key={skill} className="bg-blue-100 text-blue-600 px-2 py-1 rounded text-sm">
{skill}
</span>
))}
</div>
</div>
<div>
<h3 className="font-medium text-gray-700 mb-2">Badges Earned</h3>
<div className="grid grid-cols-2 gap-2">
{profile.badges.map(badge => (
<div key={badge} className="bg-yellow-100 text-yellow-800 px-2 py-1 rounded text-sm text-center">
🏆 {badge}
</div>
))}
</div>
</div>
</motion.div>
)}
</div>
<AnimatePresence>
{selectedAssignment && <AssignmentDetailsModal />}
</AnimatePresence>
</div>
)
}
export default MobileVolunteerApp

View File

@@ -0,0 +1,678 @@
// Phase 3B: Staff Training and Adoption System for AI Platform
import React, { useState, useEffect } from 'react'
import { motion, AnimatePresence } from 'framer-motion'
interface TrainingModule {
id: string
title: string
description: string
duration: number // in minutes
difficulty: 'beginner' | 'intermediate' | 'advanced'
category: 'ai-basics' | 'system-navigation' | 'case-management' | 'reporting' | 'troubleshooting'
prerequisites: string[]
learningObjectives: string[]
completed: boolean
score?: number
lastAttempt?: Date
certificateEarned: boolean
}
interface StaffMember {
id: string
name: string
role: string
department: string
email: string
startDate: Date
trainingProgress: number
completedModules: string[]
certificatesEarned: string[]
lastLogin?: Date
proficiencyLevel: 'novice' | 'competent' | 'proficient' | 'expert'
}
// Training session interface for future implementation
interface OnboardingChecklist {
id: string
staffId: string
items: ChecklistItem[]
completedItems: number
totalItems: number
assignedMentor?: string
startDate: Date
targetCompletionDate: Date
}
interface ChecklistItem {
id: string
title: string
description: string
category: 'account-setup' | 'training' | 'practice' | 'certification' | 'mentoring'
completed: boolean
completedDate?: Date
notes?: string
required: boolean
}
const StaffTrainingDashboard: React.FC = () => {
const [staff, setStaff] = useState<StaffMember[]>([])
const [modules, setModules] = useState<TrainingModule[]>([])
const [currentView, setCurrentView] = useState<'overview' | 'training' | 'progress' | 'onboarding'>('overview')
// const [selectedStaff, setSelectedStaff] = useState<StaffMember | null>(null) // For future staff detail view
const [selectedModule, setSelectedModule] = useState<TrainingModule | null>(null)
const [onboardingData, setOnboardingData] = useState<OnboardingChecklist[]>([])
const [loading, setLoading] = useState(true)
useEffect(() => {
loadTrainingData()
}, [])
const loadTrainingData = async () => {
setLoading(true)
// Simulate loading training modules
const trainingModules: TrainingModule[] = [
{
id: 'mod-001',
title: 'Introduction to AI-Powered Student Assistance',
description: 'Learn the basics of how our AI system helps match student needs with available resources.',
duration: 30,
difficulty: 'beginner',
category: 'ai-basics',
prerequisites: [],
learningObjectives: [
'Understand the purpose and benefits of AI assistance',
'Identify key components of the AI system',
'Recognize when AI recommendations are most valuable'
],
completed: false,
certificateEarned: false
},
{
id: 'mod-002',
title: 'Navigating the AI Portal Interface',
description: 'Master the AI portal interface, including request submission, review queues, and status monitoring.',
duration: 45,
difficulty: 'beginner',
category: 'system-navigation',
prerequisites: ['mod-001'],
learningObjectives: [
'Navigate all sections of the AI portal',
'Submit and track assistance requests',
'Interpret AI confidence scores and recommendations'
],
completed: false,
certificateEarned: false
},
{
id: 'mod-003',
title: 'Case Management with AI Assistance',
description: 'Learn to effectively manage student cases using AI recommendations and Salesforce integration.',
duration: 60,
difficulty: 'intermediate',
category: 'case-management',
prerequisites: ['mod-001', 'mod-002'],
learningObjectives: [
'Create and update cases in Salesforce',
'Evaluate AI matching recommendations',
'Coordinate with volunteers and resource providers',
'Track case outcomes and impact'
],
completed: false,
certificateEarned: false
},
{
id: 'mod-004',
title: 'Advanced Analytics and Reporting',
description: 'Utilize the analytics dashboard to track impact, identify trends, and generate reports.',
duration: 50,
difficulty: 'intermediate',
category: 'reporting',
prerequisites: ['mod-003'],
learningObjectives: [
'Generate impact reports using the dashboard',
'Identify trends in student assistance needs',
'Use predictive analytics for resource planning',
'Create custom reports for stakeholders'
],
completed: false,
certificateEarned: false
},
{
id: 'mod-005',
title: 'Troubleshooting and System Optimization',
description: 'Handle common issues, optimize AI performance, and maintain data quality.',
duration: 40,
difficulty: 'advanced',
category: 'troubleshooting',
prerequisites: ['mod-004'],
learningObjectives: [
'Diagnose and resolve common system issues',
'Optimize AI model performance through feedback',
'Maintain data quality and integrity',
'Escalate complex technical problems appropriately'
],
completed: false,
certificateEarned: false
}
]
// Simulate loading staff data
const staffMembers: StaffMember[] = [
{
id: 'staff-001',
name: 'Jennifer Martinez',
role: 'Case Manager',
department: 'Student Services',
email: 'jennifer.m@miraclesinmotion.org',
startDate: new Date('2024-01-15'),
trainingProgress: 80,
completedModules: ['mod-001', 'mod-002', 'mod-003'],
certificatesEarned: ['AI Basics Certified'],
lastLogin: new Date('2024-10-04'),
proficiencyLevel: 'competent'
},
{
id: 'staff-002',
name: 'Michael Chen',
role: 'Volunteer Coordinator',
department: 'Operations',
email: 'michael.c@miraclesinmotion.org',
startDate: new Date('2023-08-20'),
trainingProgress: 100,
completedModules: ['mod-001', 'mod-002', 'mod-003', 'mod-004', 'mod-005'],
certificatesEarned: ['AI Expert Certified', 'Advanced Analytics Certified'],
lastLogin: new Date('2024-10-05'),
proficiencyLevel: 'expert'
},
{
id: 'staff-003',
name: 'Sarah Williams',
role: 'Program Manager',
department: 'Programs',
email: 'sarah.w@miraclesinmotion.org',
startDate: new Date('2024-09-01'),
trainingProgress: 40,
completedModules: ['mod-001', 'mod-002'],
certificatesEarned: [],
lastLogin: new Date('2024-10-03'),
proficiencyLevel: 'novice'
},
{
id: 'staff-004',
name: 'David Rodriguez',
role: 'Data Analyst',
department: 'Analytics',
email: 'david.r@miraclesinmotion.org',
startDate: new Date('2024-02-10'),
trainingProgress: 90,
completedModules: ['mod-001', 'mod-002', 'mod-003', 'mod-004'],
certificatesEarned: ['AI Basics Certified', 'Analytics Certified'],
lastLogin: new Date('2024-10-05'),
proficiencyLevel: 'proficient'
}
]
// Simulate onboarding checklists
const onboardingChecklists: OnboardingChecklist[] = [
{
id: 'onboard-003',
staffId: 'staff-003',
completedItems: 6,
totalItems: 12,
assignedMentor: 'staff-002',
startDate: new Date('2024-09-01'),
targetCompletionDate: new Date('2024-10-15'),
items: [
{ id: 'check-001', title: 'Complete AI System Account Setup', description: 'Create login credentials and verify access', category: 'account-setup', completed: true, required: true },
{ id: 'check-002', title: 'Complete Module 1: AI Basics', description: 'Understand fundamental AI concepts', category: 'training', completed: true, required: true },
{ id: 'check-003', title: 'Complete Module 2: System Navigation', description: 'Learn to navigate the AI portal', category: 'training', completed: true, required: true },
{ id: 'check-004', title: 'Shadow Experienced Case Manager', description: 'Observe real case management workflows', category: 'mentoring', completed: true, required: true },
{ id: 'check-005', title: 'Process First Practice Case', description: 'Handle a low-complexity practice case', category: 'practice', completed: true, required: true },
{ id: 'check-006', title: 'Complete Module 3: Case Management', description: 'Master case management with AI assistance', category: 'training', completed: true, required: true },
{ id: 'check-007', title: 'Process 5 Real Cases Under Supervision', description: 'Gain hands-on experience with mentor oversight', category: 'practice', completed: false, required: true },
{ id: 'check-008', title: 'Complete Module 4: Analytics & Reporting', description: 'Learn to generate and interpret reports', category: 'training', completed: false, required: false },
{ id: 'check-009', title: 'Pass AI Certification Exam', description: 'Demonstrate competency in AI system usage', category: 'certification', completed: false, required: true },
{ id: 'check-010', title: 'Independent Case Processing Approval', description: 'Get approval for unsupervised case management', category: 'certification', completed: false, required: true },
{ id: 'check-011', title: 'Complete Troubleshooting Training', description: 'Learn to handle common system issues', category: 'training', completed: false, required: false },
{ id: 'check-012', title: 'Final Performance Review', description: 'Comprehensive evaluation of skills and readiness', category: 'certification', completed: false, required: true }
]
}
]
setModules(trainingModules)
setStaff(staffMembers)
setOnboardingData(onboardingChecklists)
setLoading(false)
}
const getProficiencyColor = (level: string) => {
switch (level) {
case 'expert': return 'text-purple-600 bg-purple-100'
case 'proficient': return 'text-blue-600 bg-blue-100'
case 'competent': return 'text-green-600 bg-green-100'
case 'novice': return 'text-orange-600 bg-orange-100'
default: return 'text-gray-600 bg-gray-100'
}
}
const getDifficultyColor = (difficulty: string) => {
switch (difficulty) {
case 'advanced': return 'bg-red-500'
case 'intermediate': return 'bg-yellow-500'
case 'beginner': return 'bg-green-500'
default: return 'bg-gray-500'
}
}
const getCategoryIcon = (category: string) => {
switch (category) {
case 'ai-basics': return '🧠'
case 'system-navigation': return '🗺️'
case 'case-management': return '📋'
case 'reporting': return '📊'
case 'troubleshooting': return '🔧'
default: return '📚'
}
}
if (loading) {
return (
<div className="min-h-screen bg-gradient-to-br from-purple-50 to-pink-100 flex items-center justify-center">
<motion.div
animate={{ rotate: 360 }}
transition={{ duration: 2, repeat: Infinity, ease: 'linear' }}
className="w-12 h-12 border-4 border-purple-500 border-t-transparent rounded-full"
/>
<span className="ml-4 text-lg text-purple-700">Loading Training System...</span>
</div>
)
}
return (
<div className="min-h-screen bg-gradient-to-br from-purple-50 to-pink-100 p-6">
<div className="max-w-7xl mx-auto">
{/* Header */}
<motion.div
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
className="mb-8"
>
<h1 className="text-4xl font-bold text-gray-900 mb-2">AI Training & Adoption Center</h1>
<p className="text-lg text-gray-600">Empowering our team with comprehensive AI system training</p>
<div className="flex gap-4 mt-4">
{(['overview', 'training', 'progress', 'onboarding'] as const).map((view) => (
<button
key={view}
onClick={() => setCurrentView(view)}
className={`px-4 py-2 rounded-lg font-medium transition-all ${
currentView === view
? 'bg-purple-500 text-white shadow-lg'
: 'bg-white text-gray-700 hover:bg-purple-50'
}`}
>
{view.charAt(0).toUpperCase() + view.slice(1)}
</button>
))}
</div>
</motion.div>
{/* Overview Dashboard */}
{currentView === 'overview' && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8"
>
<div className="bg-white rounded-xl p-6 shadow-lg">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold text-gray-700">Total Staff</h3>
<span className="text-2xl">👥</span>
</div>
<div className="text-3xl font-bold text-purple-600 mb-2">{staff.length}</div>
<div className="text-sm text-gray-500">Across all departments</div>
</div>
<div className="bg-white rounded-xl p-6 shadow-lg">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold text-gray-700">Avg. Progress</h3>
<span className="text-2xl">📈</span>
</div>
<div className="text-3xl font-bold text-green-600 mb-2">
{Math.round(staff.reduce((acc, s) => acc + s.trainingProgress, 0) / staff.length)}%
</div>
<div className="text-sm text-gray-500">Training completion</div>
</div>
<div className="bg-white rounded-xl p-6 shadow-lg">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold text-gray-700">Certificates</h3>
<span className="text-2xl">🏆</span>
</div>
<div className="text-3xl font-bold text-blue-600 mb-2">
{staff.reduce((acc, s) => acc + s.certificatesEarned.length, 0)}
</div>
<div className="text-sm text-gray-500">Total earned</div>
</div>
<div className="bg-white rounded-xl p-6 shadow-lg">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold text-gray-700">Experts</h3>
<span className="text-2xl"></span>
</div>
<div className="text-3xl font-bold text-orange-600 mb-2">
{staff.filter(s => s.proficiencyLevel === 'expert' || s.proficiencyLevel === 'proficient').length}
</div>
<div className="text-sm text-gray-500">Proficient+ level</div>
</div>
</motion.div>
)}
{/* Training Modules */}
{currentView === 'training' && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
className="grid grid-cols-1 lg:grid-cols-2 gap-6"
>
{modules.map((module) => (
<div key={module.id} className="bg-white rounded-xl p-6 shadow-lg">
<div className="flex items-start justify-between mb-4">
<div className="flex items-center">
<span className="text-2xl mr-3">{getCategoryIcon(module.category)}</span>
<div>
<h3 className="text-lg font-semibold text-gray-900">{module.title}</h3>
<p className="text-sm text-gray-500">{module.duration} minutes</p>
</div>
</div>
<span className={`px-2 py-1 rounded text-xs text-white ${getDifficultyColor(module.difficulty)}`}>
{module.difficulty}
</span>
</div>
<p className="text-gray-600 mb-4 text-sm">{module.description}</p>
<div className="mb-4">
<h4 className="font-medium text-gray-700 mb-2">Learning Objectives:</h4>
<ul className="text-sm text-gray-600 space-y-1">
{module.learningObjectives.slice(0, 2).map((objective, index) => (
<li key={index} className="flex items-start">
<span className="text-green-500 mr-2"></span>
{objective}
</li>
))}
{module.learningObjectives.length > 2 && (
<li className="text-gray-400">+{module.learningObjectives.length - 2} more...</li>
)}
</ul>
</div>
<div className="flex justify-between items-center">
<div className="text-sm text-gray-500">
Prerequisites: {module.prerequisites.length > 0 ? module.prerequisites.length : 'None'}
</div>
<button
onClick={() => setSelectedModule(module)}
className="px-4 py-2 bg-purple-500 text-white rounded-lg font-medium hover:bg-purple-600 transition-colors"
>
View Details
</button>
</div>
</div>
))}
</motion.div>
)}
{/* Staff Progress */}
{currentView === 'progress' && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
className="bg-white rounded-xl p-6 shadow-lg"
>
<h2 className="text-2xl font-bold text-gray-900 mb-6">Staff Training Progress</h2>
<div className="space-y-4">
{staff.map((member) => (
<div key={member.id} className="border rounded-lg p-4">
<div className="flex items-center justify-between mb-3">
<div>
<h3 className="font-semibold text-gray-900">{member.name}</h3>
<p className="text-sm text-gray-600">{member.role} {member.department}</p>
</div>
<div className="text-right">
<span className={`px-2 py-1 rounded text-xs font-medium ${getProficiencyColor(member.proficiencyLevel)}`}>
{member.proficiencyLevel.toUpperCase()}
</span>
<div className="text-sm text-gray-500 mt-1">
Last login: {member.lastLogin?.toLocaleDateString() || 'Never'}
</div>
</div>
</div>
<div className="mb-3">
<div className="flex justify-between text-sm text-gray-600 mb-1">
<span>Training Progress</span>
<span>{member.trainingProgress}%</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-2">
<motion.div
initial={{ width: 0 }}
animate={{ width: `${member.trainingProgress}%` }}
transition={{ delay: 0.2, duration: 1 }}
className="bg-purple-500 h-2 rounded-full"
/>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 text-sm">
<div>
<span className="text-gray-500">Modules Completed:</span>
<div className="font-medium">{member.completedModules.length}/{modules.length}</div>
</div>
<div>
<span className="text-gray-500">Certificates:</span>
<div className="font-medium">{member.certificatesEarned.length}</div>
</div>
<div>
<span className="text-gray-500">Start Date:</span>
<div className="font-medium">{member.startDate.toLocaleDateString()}</div>
</div>
</div>
{member.certificatesEarned.length > 0 && (
<div className="mt-3">
<div className="flex flex-wrap gap-2">
{member.certificatesEarned.map(cert => (
<span key={cert} className="bg-yellow-100 text-yellow-800 px-2 py-1 rounded text-xs">
🏆 {cert}
</span>
))}
</div>
</div>
)}
</div>
))}
</div>
</motion.div>
)}
{/* Onboarding Checklist */}
{currentView === 'onboarding' && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
className="space-y-6"
>
{onboardingData.map((checklist) => {
const staffMember = staff.find(s => s.id === checklist.staffId)
const mentor = staff.find(s => s.id === checklist.assignedMentor)
return (
<div key={checklist.id} className="bg-white rounded-xl p-6 shadow-lg">
<div className="flex items-center justify-between mb-6">
<div>
<h2 className="text-xl font-bold text-gray-900">
Onboarding: {staffMember?.name}
</h2>
<p className="text-gray-600">
{staffMember?.role} Mentor: {mentor?.name}
</p>
</div>
<div className="text-right">
<div className="text-2xl font-bold text-purple-600">
{checklist.completedItems}/{checklist.totalItems}
</div>
<div className="text-sm text-gray-500">Items Complete</div>
</div>
</div>
<div className="mb-6">
<div className="flex justify-between text-sm text-gray-600 mb-2">
<span>Overall Progress</span>
<span>{Math.round((checklist.completedItems / checklist.totalItems) * 100)}%</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-3">
<motion.div
initial={{ width: 0 }}
animate={{ width: `${(checklist.completedItems / checklist.totalItems) * 100}%` }}
transition={{ delay: 0.3, duration: 1.2 }}
className="bg-gradient-to-r from-purple-500 to-pink-500 h-3 rounded-full"
/>
</div>
</div>
<div className="grid gap-3">
{checklist.items.map((item) => (
<div
key={item.id}
className={`p-3 rounded-lg border ${
item.completed
? 'bg-green-50 border-green-200'
: item.required
? 'bg-red-50 border-red-200'
: 'bg-gray-50 border-gray-200'
}`}
>
<div className="flex items-start justify-between">
<div className="flex-1">
<div className="flex items-center">
<span className={`mr-2 ${item.completed ? '✅' : '⏳'}`}>
{item.completed ? '✅' : '⏳'}
</span>
<h4 className="font-medium text-gray-900">{item.title}</h4>
{item.required && (
<span className="ml-2 text-xs bg-red-100 text-red-600 px-1 py-0.5 rounded">
Required
</span>
)}
</div>
<p className="text-sm text-gray-600 mt-1">{item.description}</p>
{item.completed && item.completedDate && (
<p className="text-xs text-green-600 mt-1">
Completed: {item.completedDate.toLocaleDateString()}
</p>
)}
</div>
<span className={`px-2 py-1 rounded text-xs font-medium ${
item.category === 'certification' ? 'bg-purple-100 text-purple-600' :
item.category === 'training' ? 'bg-blue-100 text-blue-600' :
item.category === 'practice' ? 'bg-green-100 text-green-600' :
item.category === 'mentoring' ? 'bg-orange-100 text-orange-600' :
'bg-gray-100 text-gray-600'
}`}>
{item.category.replace('-', ' ')}
</span>
</div>
</div>
))}
</div>
</div>
)
})}
</motion.div>
)}
</div>
{/* Module Details Modal */}
<AnimatePresence>
{selectedModule && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50"
onClick={() => setSelectedModule(null)}
>
<motion.div
initial={{ scale: 0.9, y: 20 }}
animate={{ scale: 1, y: 0 }}
exit={{ scale: 0.9, y: 20 }}
className="bg-white rounded-lg max-w-2xl w-full max-h-[80vh] overflow-y-auto"
onClick={(e) => e.stopPropagation()}
>
<div className="p-6">
<div className="flex justify-between items-start mb-4">
<h2 className="text-2xl font-bold text-gray-900">{selectedModule.title}</h2>
<button
onClick={() => setSelectedModule(null)}
className="text-gray-400 hover:text-gray-600"
>
</button>
</div>
<div className="mb-6">
<p className="text-gray-600 mb-4">{selectedModule.description}</p>
<div className="grid grid-cols-2 gap-4 text-sm">
<div>
<span className="font-medium text-gray-700">Duration:</span>
<div>{selectedModule.duration} minutes</div>
</div>
<div>
<span className="font-medium text-gray-700">Difficulty:</span>
<div className="capitalize">{selectedModule.difficulty}</div>
</div>
<div>
<span className="font-medium text-gray-700">Category:</span>
<div className="capitalize">{selectedModule.category.replace('-', ' ')}</div>
</div>
<div>
<span className="font-medium text-gray-700">Prerequisites:</span>
<div>{selectedModule.prerequisites.length > 0 ? selectedModule.prerequisites.length : 'None'}</div>
</div>
</div>
</div>
<div className="mb-6">
<h3 className="font-semibold text-gray-900 mb-3">Learning Objectives</h3>
<ul className="space-y-2">
{selectedModule.learningObjectives.map((objective, index) => (
<li key={index} className="flex items-start text-sm text-gray-600">
<span className="text-green-500 mr-2"></span>
{objective}
</li>
))}
</ul>
</div>
<div className="flex gap-3">
<button className="flex-1 bg-purple-500 text-white py-3 px-4 rounded-lg font-medium hover:bg-purple-600 transition-colors">
Start Training
</button>
<button className="px-4 py-3 border border-gray-300 rounded-lg font-medium text-gray-700 hover:bg-gray-50 transition-colors">
Preview Content
</button>
</div>
</div>
</motion.div>
</motion.div>
)}
</AnimatePresence>
</div>
)
}
export default StaffTrainingDashboard