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:
71
src/App.tsx
71
src/App.tsx
@@ -62,6 +62,11 @@ import {
|
||||
// Phase 3: AI Components
|
||||
import AIAssistancePortal from './components/AIAssistancePortal'
|
||||
|
||||
// Phase 3B: Enterprise Components
|
||||
import AdvancedAnalyticsDashboard from './components/AdvancedAnalyticsDashboard'
|
||||
import MobileVolunteerApp from './components/MobileVolunteerApp'
|
||||
import StaffTrainingDashboard from './components/StaffTrainingDashboard'
|
||||
|
||||
/**
|
||||
* Miracles in Motion — Complete Non-Profit Website
|
||||
* A comprehensive 501(c)3 organization website with modern design,
|
||||
@@ -4153,6 +4158,66 @@ function AIPortalPage() {
|
||||
)
|
||||
}
|
||||
|
||||
// Phase 3B: Enterprise Feature Pages
|
||||
function AdvancedAnalyticsPage() {
|
||||
const { user, logout } = useAuth()
|
||||
|
||||
useEffect(() => {
|
||||
trackEvent('advanced_analytics_view', { user_id: user?.id, user_role: user?.role })
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<PortalWrapper requiredRole="admin">
|
||||
<SEOHead title="Advanced Analytics" description="Comprehensive impact analytics and predictive insights for nonprofit operations." />
|
||||
<div className="relative">
|
||||
<AdvancedAnalyticsDashboard />
|
||||
<div className="fixed top-4 right-4 z-50">
|
||||
<button onClick={logout} className="btn-secondary">
|
||||
Sign Out
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</PortalWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
function MobileVolunteerPage() {
|
||||
const { user } = useAuth()
|
||||
|
||||
useEffect(() => {
|
||||
trackEvent('mobile_volunteer_view', { user_id: user?.id })
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<SEOHead title="Volunteer Mobile App" description="Mobile interface for volunteer assignment management and coordination." />
|
||||
<MobileVolunteerApp />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function StaffTrainingPage() {
|
||||
const { user, logout } = useAuth()
|
||||
|
||||
useEffect(() => {
|
||||
trackEvent('staff_training_view', { user_id: user?.id, user_role: user?.role })
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<PortalWrapper requiredRole="admin">
|
||||
<SEOHead title="Staff Training & Adoption" description="Comprehensive training system for AI platform adoption and staff development." />
|
||||
<div className="relative">
|
||||
<StaffTrainingDashboard />
|
||||
<div className="fixed top-4 right-4 z-50">
|
||||
<button onClick={logout} className="btn-secondary">
|
||||
Sign Out
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</PortalWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
function NotFoundPage() {
|
||||
return (
|
||||
<section className="relative py-24">
|
||||
@@ -4395,6 +4460,12 @@ function AppContent() {
|
||||
return <AnalyticsDashboard />
|
||||
case '/ai-portal':
|
||||
return <AIPortalPage />
|
||||
case '/advanced-analytics':
|
||||
return <AdvancedAnalyticsPage />
|
||||
case '/mobile-volunteer':
|
||||
return <MobileVolunteerPage />
|
||||
case '/staff-training':
|
||||
return <StaffTrainingPage />
|
||||
default:
|
||||
return <NotFoundPage />
|
||||
}
|
||||
|
||||
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
|
||||
312
src/crm/SalesforceConnector.ts
Normal file
312
src/crm/SalesforceConnector.ts
Normal file
@@ -0,0 +1,312 @@
|
||||
// Phase 3B: Salesforce Nonprofit Cloud CRM Integration
|
||||
import type { StudentRequest, MatchResult } from '../ai/types'
|
||||
|
||||
export interface SalesforceConfig {
|
||||
instanceUrl: string
|
||||
clientId: string
|
||||
clientSecret: string
|
||||
username: string
|
||||
password: string
|
||||
securityToken: string
|
||||
apiVersion: string
|
||||
}
|
||||
|
||||
export interface SalesforceContact {
|
||||
Id: string
|
||||
Name: string
|
||||
Email: string
|
||||
Phone: string
|
||||
Account: {
|
||||
Id: string
|
||||
Name: string
|
||||
}
|
||||
}
|
||||
|
||||
export interface SalesforceCase {
|
||||
Id: string
|
||||
Subject: string
|
||||
Description: string
|
||||
Status: 'New' | 'In Progress' | 'Closed' | 'Escalated'
|
||||
Priority: 'Low' | 'Medium' | 'High' | 'Critical'
|
||||
ContactId: string
|
||||
CaseNumber: string
|
||||
CreatedDate: string
|
||||
LastModifiedDate: string
|
||||
}
|
||||
|
||||
export interface NPSPAllocation {
|
||||
Id: string
|
||||
Amount: number
|
||||
GAU__c: string // General Accounting Unit
|
||||
Opportunity__c: string
|
||||
Percent: number
|
||||
}
|
||||
|
||||
class SalesforceConnector {
|
||||
private config: SalesforceConfig
|
||||
private accessToken: string | null = null
|
||||
private instanceUrl: string = ''
|
||||
|
||||
constructor(config: SalesforceConfig) {
|
||||
this.config = config
|
||||
this.instanceUrl = config.instanceUrl
|
||||
}
|
||||
|
||||
// Authentication with Salesforce
|
||||
async authenticate(): Promise<boolean> {
|
||||
try {
|
||||
const response = await fetch(`${this.config.instanceUrl}/services/oauth2/token`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
},
|
||||
body: new URLSearchParams({
|
||||
grant_type: 'password',
|
||||
client_id: this.config.clientId,
|
||||
client_secret: this.config.clientSecret,
|
||||
username: this.config.username,
|
||||
password: this.config.password + this.config.securityToken
|
||||
})
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Authentication failed: ${response.statusText}`)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
this.accessToken = data.access_token
|
||||
this.instanceUrl = data.instance_url
|
||||
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error('Salesforce authentication error:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Create assistance request case in Salesforce
|
||||
async createAssistanceCase(request: StudentRequest): Promise<string | null> {
|
||||
if (!this.accessToken) {
|
||||
await this.authenticate()
|
||||
}
|
||||
|
||||
try {
|
||||
const caseData = {
|
||||
Subject: `Student Assistance Request - ${request.category}`,
|
||||
Description: this.formatRequestDescription(request),
|
||||
Status: 'New',
|
||||
Priority: this.determinePriority(request),
|
||||
Origin: 'AI Portal',
|
||||
Type: 'Student Assistance',
|
||||
// Custom fields for nonprofit
|
||||
Student_Name__c: request.studentName,
|
||||
Student_ID__c: request.studentId,
|
||||
Need_Category__c: request.category,
|
||||
Urgency_Level__c: request.urgency,
|
||||
Location_City__c: request.location.city,
|
||||
Location_State__c: request.location.state,
|
||||
Location_Zip__c: request.location.zipCode
|
||||
}
|
||||
|
||||
const response = await fetch(`${this.instanceUrl}/services/data/v${this.config.apiVersion}/sobjects/Case`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${this.accessToken}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(caseData)
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to create case: ${response.statusText}`)
|
||||
}
|
||||
|
||||
const result = await response.json()
|
||||
return result.id
|
||||
} catch (error) {
|
||||
console.error('Error creating Salesforce case:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
// Update case with AI matching results
|
||||
async updateCaseWithMatching(caseId: string, matchResult: MatchResult): Promise<boolean> {
|
||||
if (!this.accessToken) {
|
||||
await this.authenticate()
|
||||
}
|
||||
|
||||
try {
|
||||
const updateData = {
|
||||
AI_Match_Confidence__c: matchResult.confidenceScore,
|
||||
Recommended_Resource_Id__c: matchResult.resourceId,
|
||||
Recommended_Resource_Name__c: matchResult.resourceName,
|
||||
Resource_Type__c: matchResult.resourceType,
|
||||
Estimated_Impact__c: matchResult.estimatedImpact,
|
||||
Estimated_Cost__c: matchResult.estimatedCost,
|
||||
Fulfillment_Timeline__c: matchResult.fulfillmentTimeline,
|
||||
Last_AI_Update__c: new Date().toISOString()
|
||||
}
|
||||
|
||||
const response = await fetch(`${this.instanceUrl}/services/data/v${this.config.apiVersion}/sobjects/Case/${caseId}`, {
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${this.accessToken}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(updateData)
|
||||
})
|
||||
|
||||
return response.ok
|
||||
} catch (error) {
|
||||
console.error('Error updating Salesforce case:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Get nonprofit contacts (volunteers, donors, partners)
|
||||
async getContacts(recordType?: string): Promise<SalesforceContact[]> {
|
||||
if (!this.accessToken) {
|
||||
await this.authenticate()
|
||||
}
|
||||
|
||||
try {
|
||||
let query = `SELECT Id, Name, Email, Phone, Account.Id, Account.Name FROM Contact`
|
||||
|
||||
if (recordType) {
|
||||
query += ` WHERE RecordType.Name = '${recordType}'`
|
||||
}
|
||||
|
||||
const response = await fetch(
|
||||
`${this.instanceUrl}/services/data/v${this.config.apiVersion}/query?q=${encodeURIComponent(query)}`,
|
||||
{
|
||||
headers: {
|
||||
'Authorization': `Bearer ${this.accessToken}`
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch contacts: ${response.statusText}`)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
return data.records
|
||||
} catch (error) {
|
||||
console.error('Error fetching Salesforce contacts:', error)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
// Create NPSP allocation for resource tracking
|
||||
async createResourceAllocation(opportunityId: string, amount: number, gauId: string): Promise<string | null> {
|
||||
if (!this.accessToken) {
|
||||
await this.authenticate()
|
||||
}
|
||||
|
||||
try {
|
||||
const allocationData = {
|
||||
Amount__c: amount,
|
||||
GAU__c: gauId,
|
||||
Opportunity__c: opportunityId,
|
||||
Percent__c: 100
|
||||
}
|
||||
|
||||
const response = await fetch(`${this.instanceUrl}/services/data/v${this.config.apiVersion}/sobjects/Allocation__c`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${this.accessToken}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(allocationData)
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to create allocation: ${response.statusText}`)
|
||||
}
|
||||
|
||||
const result = await response.json()
|
||||
return result.id
|
||||
} catch (error) {
|
||||
console.error('Error creating resource allocation:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
// Get donation opportunities for matching
|
||||
async getDonationOpportunities(category?: string): Promise<any[]> {
|
||||
if (!this.accessToken) {
|
||||
await this.authenticate()
|
||||
}
|
||||
|
||||
try {
|
||||
let query = `SELECT Id, Name, Amount, StageName, CloseDate, Account.Name
|
||||
FROM Opportunity
|
||||
WHERE StageName IN ('Pledged', 'Posted')
|
||||
AND CloseDate >= TODAY`
|
||||
|
||||
if (category) {
|
||||
query += ` AND Category__c = '${category}'`
|
||||
}
|
||||
|
||||
const response = await fetch(
|
||||
`${this.instanceUrl}/services/data/v${this.config.apiVersion}/query?q=${encodeURIComponent(query)}`,
|
||||
{
|
||||
headers: {
|
||||
'Authorization': `Bearer ${this.accessToken}`
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch opportunities: ${response.statusText}`)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
return data.records
|
||||
} catch (error) {
|
||||
console.error('Error fetching donation opportunities:', error)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
private formatRequestDescription(request: StudentRequest): string {
|
||||
return `
|
||||
AI-Generated Student Assistance Request
|
||||
|
||||
Student Information:
|
||||
- Name: ${request.studentName}
|
||||
- ID: ${request.studentId}
|
||||
- Location: ${request.location.city}, ${request.location.state} ${request.location.zipCode}
|
||||
|
||||
Request Details:
|
||||
- Category: ${request.category}
|
||||
- Urgency: ${request.urgency}
|
||||
- Description: ${request.description}
|
||||
|
||||
Additional Information:
|
||||
- Estimated Cost: $${request.estimatedCost || 0}
|
||||
- Required Skills: ${request.requiredSkills?.join(', ') || 'None specified'}
|
||||
- Deadline: ${request.deadline ? new Date(request.deadline).toLocaleDateString() : 'Not specified'}
|
||||
|
||||
Submission Details:
|
||||
- Submitted: ${new Date(request.submittedAt).toLocaleString()}
|
||||
- Request ID: ${request.id}
|
||||
`.trim()
|
||||
}
|
||||
|
||||
private determinePriority(request: StudentRequest): 'Low' | 'Medium' | 'High' | 'Critical' {
|
||||
switch (request.urgency) {
|
||||
case 'emergency':
|
||||
return 'Critical'
|
||||
case 'high':
|
||||
return 'High'
|
||||
case 'medium':
|
||||
return 'Medium'
|
||||
case 'low':
|
||||
default:
|
||||
return 'Low'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export { SalesforceConnector }
|
||||
264
src/index.css
264
src/index.css
@@ -1,6 +1,6 @@
|
||||
@tailwind base;
|
||||
/* @tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
@tailwind utilities; */
|
||||
|
||||
@layer base {
|
||||
html {
|
||||
@@ -34,30 +34,170 @@
|
||||
@layer components {
|
||||
/* Button Components */
|
||||
.btn-primary {
|
||||
@apply inline-flex items-center gap-2 rounded-full bg-gradient-to-r from-primary-600 to-secondary-600 px-6 py-3 text-sm font-medium text-white shadow-lg shadow-primary-500/25 transition hover:scale-105 hover:shadow-xl focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
border-radius: 9999px;
|
||||
background-image: linear-gradient(to right, #db2777, #7c3aed); /* Replace with your primary-600 and secondary-600 colors */
|
||||
padding-left: 1.5rem;
|
||||
padding-right: 1.5rem;
|
||||
padding-top: 0.75rem;
|
||||
padding-bottom: 0.75rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
color: #fff;
|
||||
box-shadow: 0 10px 15px -3px rgba(236, 72, 153, 0.25), 0 4px 6px -4px rgba(236, 72, 153, 0.25);
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
outline: none;
|
||||
}
|
||||
.btn-primary:hover {
|
||||
transform: scale(1.05);
|
||||
box-shadow: 0 20px 25px -5px rgba(236, 72, 153, 0.25), 0 8px 10px -6px rgba(236, 72, 153, 0.25);
|
||||
}
|
||||
.btn-primary:focus {
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 2px #ec4899, 0 10px 15px -3px rgba(236, 72, 153, 0.25), 0 4px 6px -4px rgba(236, 72, 153, 0.25);
|
||||
}
|
||||
.btn-primary:focus-visible {
|
||||
outline: 2px solid #ec4899;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
@apply inline-flex items-center gap-2 rounded-full border border-neutral-300 bg-white/70 px-6 py-3 text-sm font-medium text-neutral-700 backdrop-blur transition hover:bg-white hover:shadow-lg focus:outline-none focus:ring-2 focus:ring-neutral-500 focus:ring-offset-2 dark:border-white/20 dark:bg-white/10 dark:text-neutral-200 dark:hover:bg-white/20;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
border-radius: 9999px;
|
||||
border: 1px solid #d4d4d8; /* border-neutral-300 */
|
||||
background-color: rgba(255,255,255,0.7); /* bg-white/70 */
|
||||
padding-left: 1.5rem; /* px-6 */
|
||||
padding-right: 1.5rem;
|
||||
padding-top: 0.75rem; /* py-3 */
|
||||
padding-bottom: 0.75rem;
|
||||
font-size: 0.875rem; /* text-sm */
|
||||
font-weight: 500; /* font-medium */
|
||||
color: #52525b; /* text-neutral-700 */
|
||||
backdrop-filter: blur(8px); /* backdrop-blur */
|
||||
transition: background 0.2s, box-shadow 0.2s;
|
||||
outline: none;
|
||||
}
|
||||
.btn-secondary:hover {
|
||||
background-color: #fff;
|
||||
box-shadow: 0 10px 15px -3px rgba(0,0,0,0.1), 0 4px 6px -4px rgba(0,0,0,0.1);
|
||||
}
|
||||
.btn-secondary:focus {
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 2px #737373, 0 10px 15px -3px rgba(0,0,0,0.1), 0 4px 6px -4px rgba(0,0,0,0.1);
|
||||
}
|
||||
.btn-secondary:focus-visible {
|
||||
outline: 2px solid #737373; /* focus:ring-neutral-500 */
|
||||
outline-offset: 2px;
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.btn-secondary {
|
||||
border: 1px solid rgba(255,255,255,0.2); /* dark:border-white/20 */
|
||||
background-color: rgba(255,255,255,0.1); /* dark:bg-white/10 */
|
||||
color: #e5e5e5; /* dark:text-neutral-200 */
|
||||
}
|
||||
.btn-secondary:hover {
|
||||
background-color: rgba(255,255,255,0.2); /* dark:hover:bg-white/20 */
|
||||
}
|
||||
}
|
||||
|
||||
.btn-white {
|
||||
@apply inline-flex items-center gap-2 rounded-full bg-white px-6 py-3 text-sm font-medium text-neutral-900 shadow-lg transition hover:scale-105 hover:shadow-xl focus:outline-none focus:ring-2 focus:ring-white focus:ring-offset-2;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
border-radius: 9999px;
|
||||
background-color: #fff;
|
||||
padding-left: 1.5rem;
|
||||
padding-right: 1.5rem;
|
||||
padding-top: 0.75rem;
|
||||
padding-bottom: 0.75rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
color: #18181b;
|
||||
box-shadow: 0 10px 15px -3px rgba(0,0,0,0.1), 0 4px 6px -4px rgba(0,0,0,0.1);
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
outline: none;
|
||||
}
|
||||
.btn-white:hover {
|
||||
transform: scale(1.05);
|
||||
box-shadow: 0 20px 25px -5px rgba(0,0,0,0.1), 0 8px 10px -6px rgba(0,0,0,0.1);
|
||||
}
|
||||
.btn-white:focus {
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 2px #fff, 0 10px 15px -3px rgba(0,0,0,0.1), 0 4px 6px -4px rgba(0,0,0,0.1);
|
||||
}
|
||||
.btn-white:focus-visible {
|
||||
outline: 2px solid #fff;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.navlink {
|
||||
@apply text-sm font-medium text-neutral-600 transition hover:text-primary-600 dark:text-neutral-300 dark:hover:text-primary-400;
|
||||
font-size: 0.875rem; /* text-sm */
|
||||
font-weight: 500; /* font-medium */
|
||||
color: #52525b; /* text-neutral-600 */
|
||||
transition: color 0.2s;
|
||||
}
|
||||
.navlink:hover {
|
||||
color: #ec4899; /* text-primary-600 */
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.navlink {
|
||||
color: #d4d4d8; /* dark:text-neutral-300 */
|
||||
}
|
||||
.navlink:hover {
|
||||
color: #a78bfa; /* dark:hover:text-primary-400 */
|
||||
}
|
||||
}
|
||||
|
||||
/* Form Components */
|
||||
.input-field {
|
||||
@apply w-full rounded-xl border border-gray-300 bg-white px-4 py-3 text-sm text-gray-900 placeholder-gray-500 transition focus:border-primary-500 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100 dark:placeholder-gray-400 dark:focus:border-primary-400;
|
||||
width: 100%;
|
||||
border-radius: 0.75rem;
|
||||
border: 1px solid #d1d5db; /* gray-300 */
|
||||
background-color: #fff;
|
||||
padding-left: 1rem;
|
||||
padding-right: 1rem;
|
||||
padding-top: 0.75rem;
|
||||
padding-bottom: 0.75rem;
|
||||
font-size: 0.875rem;
|
||||
color: #111827; /* gray-900 */
|
||||
transition: border-color 0.2s, box-shadow 0.2s;
|
||||
}
|
||||
.input-field::placeholder {
|
||||
color: #6b7280; /* gray-500 */
|
||||
opacity: 1;
|
||||
}
|
||||
.input-field:focus {
|
||||
border-color: #ec4899; /* primary-500 */
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 2px #ec4899, 0 0 0 2px rgba(236, 72, 153, 0.2);
|
||||
}
|
||||
.input-field:focus-visible {
|
||||
outline: 2px solid #ec4899;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.input-field {
|
||||
border: 1px solid #4b5563; /* gray-600 */
|
||||
background-color: #1f2937; /* gray-800 */
|
||||
color: #f3f4f6; /* gray-100 */
|
||||
}
|
||||
.input-field::placeholder {
|
||||
color: #9ca3af; /* gray-400 */
|
||||
}
|
||||
.input-field:focus {
|
||||
border-color: #a21caf; /* primary-400 */
|
||||
}
|
||||
}
|
||||
|
||||
/* Text Utilities */
|
||||
.line-clamp-2 {
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
@@ -65,54 +205,132 @@
|
||||
.line-clamp-3 {
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 3;
|
||||
line-clamp: 3;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.input {
|
||||
@apply w-full rounded-xl border border-white/30 bg-white/70 px-3 py-2 text-sm backdrop-blur transition focus:border-primary-500 focus:outline-none focus:ring-2 focus:ring-primary-500/20 dark:border-white/10 dark:bg-white/10 dark:focus:border-primary-400;
|
||||
width: 100%;
|
||||
border-radius: 0.75rem; /* rounded-xl */
|
||||
border: 1px solid rgba(255,255,255,0.3); /* border-white/30 */
|
||||
background-color: rgba(255,255,255,0.7); /* bg-white/70 */
|
||||
padding-left: 0.75rem; /* px-3 */
|
||||
padding-right: 0.75rem;
|
||||
padding-top: 0.5rem; /* py-2 */
|
||||
padding-bottom: 0.5rem;
|
||||
font-size: 0.875rem; /* text-sm */
|
||||
backdrop-filter: blur(8px); /* backdrop-blur */
|
||||
transition: border-color 0.2s, box-shadow 0.2s;
|
||||
outline: none;
|
||||
}
|
||||
.input:focus {
|
||||
border-color: #ec4899; /* primary-500 */
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 2px #ec4899, 0 0 0 2px rgba(236, 72, 153, 0.2);
|
||||
}
|
||||
.input:focus-visible {
|
||||
outline: 2px solid #ec4899;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.input {
|
||||
border: 1px solid rgba(255,255,255,0.1); /* dark:border-white/10 */
|
||||
background-color: rgba(255,255,255,0.1); /* dark:bg-white/10 */
|
||||
}
|
||||
.input:focus {
|
||||
border-color: #a21caf; /* dark:focus:border-primary-400 */
|
||||
}
|
||||
}
|
||||
|
||||
/* Card Components */
|
||||
.card {
|
||||
@apply rounded-2xl border border-white/30 bg-white/70 p-6 shadow-xl backdrop-blur dark:border-white/10 dark:bg-white/5;
|
||||
border-radius: 1rem; /* rounded-2xl */
|
||||
border: 1px solid rgba(255,255,255,0.3); /* border-white/30 */
|
||||
background-color: rgba(255,255,255,0.7); /* bg-white/70 */
|
||||
padding: 1.5rem; /* p-6 */
|
||||
box-shadow: 0 20px 25px -5px rgba(0,0,0,0.1), 0 8px 10px -6px rgba(0,0,0,0.1); /* shadow-xl */
|
||||
backdrop-filter: blur(8px); /* backdrop-blur */
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.card {
|
||||
border: 1px solid rgba(255,255,255,0.1); /* dark:border-white/10 */
|
||||
background-color: rgba(255,255,255,0.05); /* dark:bg-white/5 */
|
||||
}
|
||||
}
|
||||
|
||||
.card-hover {
|
||||
@apply transition hover:-translate-y-1 hover:shadow-2xl;
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
}
|
||||
.card-hover:hover {
|
||||
transform: translateY(-0.25rem); /* -translate-y-1 */
|
||||
box-shadow: 0 25px 50px -12px rgba(0,0,0,0.25), 0 8px 10px -6px rgba(0,0,0,0.1); /* shadow-2xl */
|
||||
}
|
||||
|
||||
/* Section Header */
|
||||
.section-header {
|
||||
@apply mx-auto max-w-3xl text-center;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
max-width: 48rem; /* 3xl = 48rem */
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.section-eyebrow {
|
||||
@apply text-sm uppercase tracking-wider text-primary-600 dark:text-primary-400;
|
||||
font-size: 0.875rem; /* text-sm */
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em; /* tracking-wider */
|
||||
color: #db2777; /* text-primary-600 */
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.section-eyebrow {
|
||||
color: #a78bfa; /* dark:text-primary-400 */
|
||||
}
|
||||
}
|
||||
|
||||
.section-title {
|
||||
@apply mt-2 text-3xl font-bold tracking-tight sm:text-4xl;
|
||||
margin-top: 0.5rem; /* mt-2 */
|
||||
font-size: 1.875rem; /* text-3xl */
|
||||
font-weight: 700; /* font-bold */
|
||||
letter-spacing: -0.025em; /* tracking-tight */
|
||||
line-height: 2.25rem;
|
||||
}
|
||||
@media (min-width: 640px) {
|
||||
.section-title {
|
||||
font-size: 2.25rem; /* sm:text-4xl */
|
||||
line-height: 2.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.section-subtitle {
|
||||
@apply mt-4 text-lg text-neutral-600 dark:text-neutral-300;
|
||||
margin-top: 1rem; /* mt-4 */
|
||||
font-size: 1.125rem; /* text-lg */
|
||||
color: #52525b; /* text-neutral-600 */
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.section-subtitle {
|
||||
color: #d4d4d8; /* dark:text-neutral-300 */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@layer utilities {
|
||||
/* Gradient Text */
|
||||
.gradient-text {
|
||||
@apply bg-gradient-to-r from-primary-500 via-primary-600 to-secondary-600 bg-clip-text text-transparent;
|
||||
background-image: linear-gradient(to right, #ec4899, #a21caf, #7c3aed); /* Replace with your Tailwind color values */
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
color: transparent;
|
||||
}
|
||||
|
||||
/* Glass Effect */
|
||||
.glass {
|
||||
@apply bg-white/10 backdrop-blur-md;
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
backdrop-filter: blur(12px);
|
||||
}
|
||||
|
||||
.glass-dark {
|
||||
@apply bg-black/10 backdrop-blur-md;
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
backdrop-filter: blur(12px);
|
||||
}
|
||||
|
||||
/* Text Balance */
|
||||
@@ -147,15 +365,21 @@
|
||||
/* High Contrast Mode Support */
|
||||
@media (prefers-contrast: high) {
|
||||
.btn-primary {
|
||||
@apply border-2 border-current;
|
||||
border-width: 2px;
|
||||
border-style: solid;
|
||||
border-color: currentColor;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
@apply border-2 border-current;
|
||||
border-width: 2px;
|
||||
border-style: solid;
|
||||
border-color: currentColor;
|
||||
}
|
||||
|
||||
.card {
|
||||
@apply border-2 border-current;
|
||||
border-width: 2px;
|
||||
border-style: solid;
|
||||
border-color: currentColor;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
460
src/services/RealTimeProcessor.ts
Normal file
460
src/services/RealTimeProcessor.ts
Normal file
@@ -0,0 +1,460 @@
|
||||
// Phase 3B: Real-Time Data Processing and Live Deployment System
|
||||
import { StudentAssistanceAI } from '../ai/StudentAssistanceAI'
|
||||
import { SalesforceConnector } from '../crm/SalesforceConnector'
|
||||
import type { StudentRequest, MatchResult, AIUpdate } from '../ai/types'
|
||||
|
||||
interface RealTimeConfig {
|
||||
enableWebSockets: boolean
|
||||
batchSize: number
|
||||
processingInterval: number
|
||||
maxConcurrentRequests: number
|
||||
enablePredictiveLoading: boolean
|
||||
cacheTimeout: number
|
||||
enableOfflineMode: boolean
|
||||
}
|
||||
|
||||
interface ProcessingMetrics {
|
||||
totalProcessed: number
|
||||
averageProcessingTime: number
|
||||
successRate: number
|
||||
errorCount: number
|
||||
queueLength: number
|
||||
activeProcessors: number
|
||||
throughputPerMinute: number
|
||||
lastProcessedAt: Date
|
||||
}
|
||||
|
||||
interface DataSyncStatus {
|
||||
salesforceSync: boolean
|
||||
databaseSync: boolean
|
||||
cacheSync: boolean
|
||||
aiModelSync: boolean
|
||||
lastSyncAt: Date
|
||||
pendingSyncCount: number
|
||||
}
|
||||
|
||||
class RealTimeProcessor {
|
||||
private ai: StudentAssistanceAI
|
||||
private salesforce?: SalesforceConnector
|
||||
private config: RealTimeConfig
|
||||
private processingQueue: StudentRequest[] = []
|
||||
private activeProcessors = new Map<string, Promise<MatchResult[]>>()
|
||||
private metrics: ProcessingMetrics
|
||||
private syncStatus: DataSyncStatus
|
||||
private subscribers: ((update: AIUpdate) => void)[] = []
|
||||
private websocket?: WebSocket
|
||||
private processTimer?: number
|
||||
private isProcessing = false
|
||||
|
||||
constructor(config: RealTimeConfig, salesforceConfig?: any) {
|
||||
this.config = config
|
||||
this.ai = new StudentAssistanceAI()
|
||||
|
||||
if (salesforceConfig) {
|
||||
this.salesforce = new SalesforceConnector(salesforceConfig)
|
||||
}
|
||||
|
||||
this.metrics = {
|
||||
totalProcessed: 0,
|
||||
averageProcessingTime: 0,
|
||||
successRate: 0,
|
||||
errorCount: 0,
|
||||
queueLength: 0,
|
||||
activeProcessors: 0,
|
||||
throughputPerMinute: 0,
|
||||
lastProcessedAt: new Date()
|
||||
}
|
||||
|
||||
this.syncStatus = {
|
||||
salesforceSync: false,
|
||||
databaseSync: false,
|
||||
cacheSync: false,
|
||||
aiModelSync: false,
|
||||
lastSyncAt: new Date(),
|
||||
pendingSyncCount: 0
|
||||
}
|
||||
|
||||
this.initialize()
|
||||
}
|
||||
|
||||
private async initialize(): Promise<void> {
|
||||
try {
|
||||
console.log('🚀 Initializing Real-Time Processing System...')
|
||||
|
||||
// Initialize AI engine
|
||||
console.log('✅ AI Engine ready')
|
||||
|
||||
// Initialize Salesforce connection
|
||||
if (this.salesforce) {
|
||||
const connected = await this.salesforce.authenticate()
|
||||
this.syncStatus.salesforceSync = connected
|
||||
console.log(connected ? '✅ Salesforce connected' : '⚠️ Salesforce connection failed')
|
||||
}
|
||||
|
||||
// Setup WebSocket for real-time updates
|
||||
if (this.config.enableWebSockets) {
|
||||
this.setupWebSocket()
|
||||
}
|
||||
|
||||
// Start processing timer
|
||||
this.startProcessingTimer()
|
||||
|
||||
console.log('🎯 Real-Time Processing System Online')
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to initialize real-time processor:', error)
|
||||
}
|
||||
}
|
||||
|
||||
private setupWebSocket(): void {
|
||||
try {
|
||||
const wsUrl = process.env.NODE_ENV === 'production'
|
||||
? 'wss://miracles-in-motion.org/websocket'
|
||||
: 'ws://localhost:8080/websocket'
|
||||
|
||||
this.websocket = new WebSocket(wsUrl)
|
||||
|
||||
this.websocket.onopen = () => {
|
||||
console.log('🔗 WebSocket connected for real-time updates')
|
||||
this.broadcastUpdate({
|
||||
type: 'model-updated',
|
||||
message: 'Real-time processing system online',
|
||||
timestamp: new Date()
|
||||
})
|
||||
}
|
||||
|
||||
this.websocket.onmessage = (event) => {
|
||||
try {
|
||||
const data = JSON.parse(event.data)
|
||||
this.handleWebSocketMessage(data)
|
||||
} catch (error) {
|
||||
console.error('Error parsing WebSocket message:', error)
|
||||
}
|
||||
}
|
||||
|
||||
this.websocket.onclose = () => {
|
||||
console.log('🔌 WebSocket disconnected, attempting reconnection...')
|
||||
setTimeout(() => this.setupWebSocket(), 5000)
|
||||
}
|
||||
|
||||
this.websocket.onerror = (error) => {
|
||||
console.error('WebSocket error:', error)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to setup WebSocket:', error)
|
||||
}
|
||||
}
|
||||
|
||||
private handleWebSocketMessage(data: any): void {
|
||||
switch (data.type) {
|
||||
case 'new-request':
|
||||
this.addToQueue(data.request)
|
||||
break
|
||||
case 'priority-update':
|
||||
this.updateRequestPriority(data.requestId, data.priority)
|
||||
break
|
||||
case 'system-status':
|
||||
this.handleSystemStatusUpdate(data)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
private startProcessingTimer(): void {
|
||||
this.processTimer = window.setInterval(() => {
|
||||
if (!this.isProcessing && this.processingQueue.length > 0) {
|
||||
this.processNextBatch()
|
||||
}
|
||||
this.updateMetrics()
|
||||
}, this.config.processingInterval)
|
||||
}
|
||||
|
||||
// Public API Methods
|
||||
public addToQueue(request: StudentRequest): void {
|
||||
this.processingQueue.push(request)
|
||||
this.metrics.queueLength = this.processingQueue.length
|
||||
|
||||
this.broadcastUpdate({
|
||||
type: 'request-processed',
|
||||
requestId: request.id,
|
||||
studentName: request.studentName,
|
||||
status: 'queued',
|
||||
message: `Request added to processing queue (${this.metrics.queueLength} pending)`,
|
||||
timestamp: new Date()
|
||||
})
|
||||
|
||||
// Trigger immediate processing if queue was empty
|
||||
if (this.processingQueue.length === 1 && !this.isProcessing) {
|
||||
setTimeout(() => this.processNextBatch(), 100)
|
||||
}
|
||||
}
|
||||
|
||||
public async processNextBatch(): Promise<void> {
|
||||
if (this.isProcessing || this.processingQueue.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
this.isProcessing = true
|
||||
const batchSize = Math.min(this.config.batchSize, this.processingQueue.length)
|
||||
const batch = this.processingQueue.splice(0, batchSize)
|
||||
|
||||
console.log(`📊 Processing batch of ${batch.length} requests...`)
|
||||
|
||||
const processingPromises = batch.map(request => this.processSingleRequest(request))
|
||||
|
||||
try {
|
||||
const results = await Promise.allSettled(processingPromises)
|
||||
|
||||
let successCount = 0
|
||||
let errorCount = 0
|
||||
|
||||
results.forEach((result, index) => {
|
||||
if (result.status === 'fulfilled') {
|
||||
successCount++
|
||||
this.handleProcessingSuccess(batch[index], result.value)
|
||||
} else {
|
||||
errorCount++
|
||||
this.handleProcessingError(batch[index], result.reason)
|
||||
}
|
||||
})
|
||||
|
||||
// Update metrics
|
||||
this.metrics.totalProcessed += batch.length
|
||||
this.metrics.errorCount += errorCount
|
||||
this.metrics.successRate = (this.metrics.totalProcessed - this.metrics.errorCount) / this.metrics.totalProcessed
|
||||
this.metrics.lastProcessedAt = new Date()
|
||||
|
||||
console.log(`✅ Batch complete: ${successCount} success, ${errorCount} errors`)
|
||||
|
||||
} catch (error) {
|
||||
console.error('Batch processing error:', error)
|
||||
} finally {
|
||||
this.isProcessing = false
|
||||
this.metrics.queueLength = this.processingQueue.length
|
||||
}
|
||||
}
|
||||
|
||||
private async processSingleRequest(request: StudentRequest): Promise<MatchResult[]> {
|
||||
const startTime = Date.now()
|
||||
|
||||
try {
|
||||
// Process with AI
|
||||
const matches = await this.ai.processRequest(request)
|
||||
|
||||
// Create Salesforce case if connected
|
||||
if (this.salesforce && matches.length > 0) {
|
||||
const caseId = await this.salesforce.createAssistanceCase(request)
|
||||
if (caseId) {
|
||||
await this.salesforce.updateCaseWithMatching(caseId, matches[0])
|
||||
}
|
||||
}
|
||||
|
||||
const processingTime = Date.now() - startTime
|
||||
this.updateAverageProcessingTime(processingTime)
|
||||
|
||||
return matches
|
||||
} catch (error) {
|
||||
console.error(`Error processing request ${request.id}:`, error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
private handleProcessingSuccess(request: StudentRequest, matches: MatchResult[]): void {
|
||||
this.broadcastUpdate({
|
||||
type: 'request-processed',
|
||||
requestId: request.id,
|
||||
studentName: request.studentName,
|
||||
status: 'completed',
|
||||
recommendations: matches,
|
||||
message: `Found ${matches.length} potential matches`,
|
||||
timestamp: new Date()
|
||||
})
|
||||
|
||||
// Auto-approve high confidence matches
|
||||
const highConfidenceMatches = matches.filter(match => match.confidenceScore >= 0.85)
|
||||
if (highConfidenceMatches.length > 0) {
|
||||
this.broadcastUpdate({
|
||||
type: 'auto-approval',
|
||||
requestId: request.id,
|
||||
studentName: request.studentName,
|
||||
message: `Auto-approved ${highConfidenceMatches.length} high-confidence matches`,
|
||||
timestamp: new Date()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private handleProcessingError(request: StudentRequest, error: any): void {
|
||||
console.error(`Processing failed for ${request.id}:`, error)
|
||||
|
||||
this.broadcastUpdate({
|
||||
type: 'alert',
|
||||
requestId: request.id,
|
||||
studentName: request.studentName,
|
||||
message: `Processing failed: ${error.message || 'Unknown error'}`,
|
||||
timestamp: new Date()
|
||||
})
|
||||
|
||||
// Re-queue with lower priority if retryable
|
||||
if (this.isRetryableError(error)) {
|
||||
setTimeout(() => {
|
||||
this.processingQueue.push({ ...request, urgency: 'low' })
|
||||
}, 5000)
|
||||
}
|
||||
}
|
||||
|
||||
private isRetryableError(error: any): boolean {
|
||||
// Define retryable error conditions
|
||||
return error.code === 'NETWORK_ERROR' ||
|
||||
error.code === 'RATE_LIMIT' ||
|
||||
error.message?.includes('timeout')
|
||||
}
|
||||
|
||||
private updateAverageProcessingTime(newTime: number): void {
|
||||
const totalProcessed = this.metrics.totalProcessed
|
||||
const currentAverage = this.metrics.averageProcessingTime
|
||||
this.metrics.averageProcessingTime = ((currentAverage * totalProcessed) + newTime) / (totalProcessed + 1)
|
||||
}
|
||||
|
||||
private updateMetrics(): void {
|
||||
const now = Date.now()
|
||||
|
||||
// Calculate throughput (simplified)
|
||||
this.metrics.throughputPerMinute = this.metrics.totalProcessed > 0 ?
|
||||
Math.round(this.metrics.totalProcessed / ((now - this.metrics.lastProcessedAt.getTime()) / 60000)) : 0
|
||||
|
||||
this.metrics.activeProcessors = this.activeProcessors.size
|
||||
}
|
||||
|
||||
private updateRequestPriority(requestId: string, newPriority: string): void {
|
||||
const requestIndex = this.processingQueue.findIndex(req => req.id === requestId)
|
||||
if (requestIndex !== -1) {
|
||||
this.processingQueue[requestIndex].urgency = newPriority as any
|
||||
|
||||
// Re-sort queue by priority
|
||||
this.processingQueue.sort((a, b) => {
|
||||
const priorityOrder = { emergency: 4, high: 3, medium: 2, low: 1 }
|
||||
return priorityOrder[b.urgency] - priorityOrder[a.urgency]
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private handleSystemStatusUpdate(data: any): void {
|
||||
// Update sync status based on external system updates
|
||||
if (data.salesforce !== undefined) {
|
||||
this.syncStatus.salesforceSync = data.salesforce
|
||||
}
|
||||
if (data.database !== undefined) {
|
||||
this.syncStatus.databaseSync = data.database
|
||||
}
|
||||
}
|
||||
|
||||
// Subscription methods for real-time updates
|
||||
public subscribe(callback: (update: AIUpdate) => void): () => void {
|
||||
this.subscribers.push(callback)
|
||||
|
||||
// Return unsubscribe function
|
||||
return () => {
|
||||
const index = this.subscribers.indexOf(callback)
|
||||
if (index > -1) {
|
||||
this.subscribers.splice(index, 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private broadcastUpdate(update: AIUpdate): void {
|
||||
this.subscribers.forEach(callback => {
|
||||
try {
|
||||
callback(update)
|
||||
} catch (error) {
|
||||
console.error('Error in subscriber callback:', error)
|
||||
}
|
||||
})
|
||||
|
||||
// Send to WebSocket if connected
|
||||
if (this.websocket && this.websocket.readyState === WebSocket.OPEN) {
|
||||
this.websocket.send(JSON.stringify(update))
|
||||
}
|
||||
}
|
||||
|
||||
// Status and metrics getters
|
||||
public getMetrics(): ProcessingMetrics {
|
||||
return { ...this.metrics }
|
||||
}
|
||||
|
||||
public getSyncStatus(): DataSyncStatus {
|
||||
return { ...this.syncStatus }
|
||||
}
|
||||
|
||||
public getQueueStatus(): { length: number; processing: boolean; nextEstimatedTime?: number } {
|
||||
return {
|
||||
length: this.processingQueue.length,
|
||||
processing: this.isProcessing,
|
||||
nextEstimatedTime: this.processingQueue.length > 0 ?
|
||||
this.metrics.averageProcessingTime * Math.ceil(this.processingQueue.length / this.config.batchSize) : undefined
|
||||
}
|
||||
}
|
||||
|
||||
// Lifecycle management
|
||||
public pause(): void {
|
||||
if (this.processTimer) {
|
||||
clearInterval(this.processTimer)
|
||||
this.processTimer = undefined
|
||||
}
|
||||
console.log('⏸️ Real-time processing paused')
|
||||
}
|
||||
|
||||
public resume(): void {
|
||||
if (!this.processTimer) {
|
||||
this.startProcessingTimer()
|
||||
console.log('▶️ Real-time processing resumed')
|
||||
}
|
||||
}
|
||||
|
||||
public async shutdown(): Promise<void> {
|
||||
console.log('🛑 Shutting down real-time processor...')
|
||||
|
||||
this.pause()
|
||||
|
||||
// Wait for active processors to complete
|
||||
if (this.activeProcessors.size > 0) {
|
||||
console.log(`⏳ Waiting for ${this.activeProcessors.size} active processors...`)
|
||||
await Promise.all(this.activeProcessors.values())
|
||||
}
|
||||
|
||||
// Close WebSocket
|
||||
if (this.websocket) {
|
||||
this.websocket.close()
|
||||
}
|
||||
|
||||
console.log('✅ Real-time processor shutdown complete')
|
||||
}
|
||||
}
|
||||
|
||||
// Factory function for easy initialization
|
||||
export const createRealTimeProcessor = (config: Partial<RealTimeConfig> = {}): RealTimeProcessor => {
|
||||
const defaultConfig: RealTimeConfig = {
|
||||
enableWebSockets: true,
|
||||
batchSize: 5,
|
||||
processingInterval: 2000,
|
||||
maxConcurrentRequests: 10,
|
||||
enablePredictiveLoading: true,
|
||||
cacheTimeout: 300000, // 5 minutes
|
||||
enableOfflineMode: true
|
||||
}
|
||||
|
||||
const finalConfig = { ...defaultConfig, ...config }
|
||||
|
||||
// Salesforce config from environment variables
|
||||
const salesforceConfig = process.env.NODE_ENV === 'production' ? {
|
||||
instanceUrl: process.env.REACT_APP_SALESFORCE_URL || '',
|
||||
clientId: process.env.REACT_APP_SALESFORCE_CLIENT_ID || '',
|
||||
clientSecret: process.env.REACT_APP_SALESFORCE_CLIENT_SECRET || '',
|
||||
username: process.env.REACT_APP_SALESFORCE_USERNAME || '',
|
||||
password: process.env.REACT_APP_SALESFORCE_PASSWORD || '',
|
||||
securityToken: process.env.REACT_APP_SALESFORCE_TOKEN || '',
|
||||
apiVersion: '58.0'
|
||||
} : undefined
|
||||
|
||||
return new RealTimeProcessor(finalConfig, salesforceConfig)
|
||||
}
|
||||
|
||||
export { RealTimeProcessor }
|
||||
export type { RealTimeConfig, ProcessingMetrics, DataSyncStatus }
|
||||
Reference in New Issue
Block a user