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

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

View File

@@ -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 />
}

View File

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

View File

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

View File

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

View 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 }

View File

@@ -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;
}
}

View 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 }