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:
370
src/components/AdvancedAnalyticsDashboard.tsx
Normal file
370
src/components/AdvancedAnalyticsDashboard.tsx
Normal 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
|
||||
509
src/components/MobileVolunteerApp.tsx
Normal file
509
src/components/MobileVolunteerApp.tsx
Normal 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
|
||||
678
src/components/StaffTrainingDashboard.tsx
Normal file
678
src/components/StaffTrainingDashboard.tsx
Normal 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
|
||||
Reference in New Issue
Block a user