- Add admin dashboard with sync controls and data browser - Implement RMM, Auvik, and Addigy organization mappings - Add chunked ticket sync with progress tracking - Implement entity sync service with rate limiting - Add analytics engine and performance optimizer - Create data browser for all PSA entities - Add navigation components and UI improvements - Implement background processing and sync services - Add comprehensive documentation and migration scripts - Update configuration items with multi-system support - Enhance contact management and purchase history - Add issue type assignment and LLM analyzer - Improve error handling and logging utilities
416 lines
16 KiB
TypeScript
416 lines
16 KiB
TypeScript
'use client';
|
|
|
|
import React, { useState, useEffect } from 'react';
|
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Badge } from '@/components/ui/badge';
|
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
|
import { ScrollArea } from '@/components/ui/scroll-area';
|
|
import { Separator } from '@/components/ui/separator';
|
|
import {
|
|
Brain,
|
|
Lightbulb,
|
|
TrendingUp,
|
|
AlertTriangle,
|
|
CheckCircle,
|
|
Info,
|
|
RefreshCw,
|
|
Download,
|
|
Filter,
|
|
Calendar,
|
|
Target,
|
|
Activity
|
|
} from 'lucide-react';
|
|
import { AnalyticsInsight, LLMAnalysisResponse } from '@/lib/types/analytics';
|
|
import { cn } from '@/lib/utils';
|
|
|
|
interface AnalysisPanelProps {
|
|
insights: AnalyticsInsight[];
|
|
llmAnalysis?: LLMAnalysisResponse;
|
|
loading?: boolean;
|
|
onRefresh?: () => void;
|
|
onExport?: () => void;
|
|
className?: string;
|
|
}
|
|
|
|
export function AnalysisPanel({
|
|
insights,
|
|
llmAnalysis,
|
|
loading = false,
|
|
onRefresh,
|
|
onExport,
|
|
className
|
|
}: AnalysisPanelProps) {
|
|
const [activeTab, setActiveTab] = useState('insights');
|
|
const [filter, setFilter] = useState<'all' | 'warnings' | 'recommendations' | 'success'>('all');
|
|
|
|
// Filter insights based on selected filter
|
|
const filteredInsights = insights.filter(insight => {
|
|
switch (filter) {
|
|
case 'warnings':
|
|
return insight.type === 'warning' || insight.type === 'error';
|
|
case 'recommendations':
|
|
return insight.actionable === true && insight.recommendation;
|
|
case 'success':
|
|
return insight.type === 'success';
|
|
default:
|
|
return true;
|
|
}
|
|
});
|
|
|
|
// Group insights by category
|
|
const insightsByCategory = filteredInsights.reduce((groups, insight) => {
|
|
if (!groups[insight.category]) {
|
|
groups[insight.category] = [];
|
|
}
|
|
groups[insight.category].push(insight);
|
|
return groups;
|
|
}, {} as Record<string, AnalyticsInsight[]>);
|
|
|
|
const getInsightIcon = (type: string) => {
|
|
switch (type) {
|
|
case 'success':
|
|
return <CheckCircle className="h-4 w-4 text-green-500" />;
|
|
case 'warning':
|
|
return <AlertTriangle className="h-4 w-4 text-yellow-500" />;
|
|
case 'error':
|
|
return <AlertTriangle className="h-4 w-4 text-red-500" />;
|
|
case 'info':
|
|
default:
|
|
return <Info className="h-4 w-4 text-blue-500" />;
|
|
}
|
|
};
|
|
|
|
const getInsightColor = (type: string) => {
|
|
switch (type) {
|
|
case 'success':
|
|
return 'border-green-200 bg-green-50';
|
|
case 'warning':
|
|
return 'border-yellow-200 bg-yellow-50';
|
|
case 'error':
|
|
return 'border-red-200 bg-red-50';
|
|
case 'info':
|
|
default:
|
|
return 'border-blue-200 bg-blue-50';
|
|
}
|
|
};
|
|
|
|
const getSeverityColor = (severity?: string) => {
|
|
switch (severity) {
|
|
case 'high':
|
|
return 'bg-red-100 text-red-800';
|
|
case 'medium':
|
|
return 'bg-yellow-100 text-yellow-800';
|
|
case 'low':
|
|
default:
|
|
return 'bg-gray-100 text-gray-800';
|
|
}
|
|
};
|
|
|
|
const getCategoryIcon = (category: string) => {
|
|
switch (category) {
|
|
case 'activity':
|
|
return <Activity className="h-4 w-4 text-blue-500" />;
|
|
case 'content':
|
|
return <Target className="h-4 w-4 text-green-500" />;
|
|
case 'timeliness':
|
|
return <Calendar className="h-4 w-4 text-orange-500" />;
|
|
case 'patterns':
|
|
return <TrendingUp className="h-4 w-4 text-purple-500" />;
|
|
case 'recommendations':
|
|
return <Lightbulb className="h-4 w-4 text-yellow-500" />;
|
|
default:
|
|
return <Info className="h-4 w-4 text-gray-500" />;
|
|
}
|
|
};
|
|
|
|
if (loading) {
|
|
return (
|
|
<Card className={className}>
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center gap-2">
|
|
<Brain className="h-5 w-5" />
|
|
AI Analysis
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="flex items-center justify-center h-64">
|
|
<div className="flex flex-col items-center gap-4">
|
|
<RefreshCw className="h-8 w-8 animate-spin text-blue-600" />
|
|
<p className="text-sm text-gray-600">Analyzing time entries...</p>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<Card className={className}>
|
|
<CardHeader>
|
|
<div className="flex items-center justify-between">
|
|
<CardTitle className="flex items-center gap-2">
|
|
<Brain className="h-5 w-5" />
|
|
AI Analysis & Insights
|
|
</CardTitle>
|
|
|
|
<div className="flex items-center gap-2">
|
|
{onRefresh && (
|
|
<Button variant="outline" size="sm" onClick={onRefresh}>
|
|
<RefreshCw className="h-4 w-4 mr-2" />
|
|
Refresh
|
|
</Button>
|
|
)}
|
|
{onExport && (
|
|
<Button variant="outline" size="sm" onClick={onExport}>
|
|
<Download className="h-4 w-4 mr-2" />
|
|
Export
|
|
</Button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Summary Stats */}
|
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mt-4">
|
|
<div className="text-center">
|
|
<div className="text-2xl font-bold text-blue-600">{insights.length}</div>
|
|
<div className="text-sm text-gray-600">Total Insights</div>
|
|
</div>
|
|
<div className="text-center">
|
|
<div className="text-2xl font-bold text-green-600">
|
|
{insights.filter(i => i.type === 'success').length}
|
|
</div>
|
|
<div className="text-sm text-gray-600">Positive</div>
|
|
</div>
|
|
<div className="text-center">
|
|
<div className="text-2xl font-bold text-yellow-600">
|
|
{insights.filter(i => i.type === 'warning').length}
|
|
</div>
|
|
<div className="text-sm text-gray-600">Warnings</div>
|
|
</div>
|
|
<div className="text-center">
|
|
<div className="text-2xl font-bold text-red-600">
|
|
{insights.filter(i => i.type === 'error').length}
|
|
</div>
|
|
<div className="text-sm text-gray-600">Issues</div>
|
|
</div>
|
|
</div>
|
|
</CardHeader>
|
|
|
|
<CardContent>
|
|
<Tabs value={activeTab} onValueChange={setActiveTab}>
|
|
<div className="flex items-center justify-between mb-4">
|
|
<TabsList>
|
|
<TabsTrigger value="insights">Insights</TabsTrigger>
|
|
<TabsTrigger value="patterns">Patterns</TabsTrigger>
|
|
<TabsTrigger value="recommendations">Recommendations</TabsTrigger>
|
|
{llmAnalysis && <TabsTrigger value="llm">AI Analysis</TabsTrigger>}
|
|
</TabsList>
|
|
|
|
{/* Filter */}
|
|
<div className="flex items-center gap-2">
|
|
<Filter className="h-4 w-4 text-gray-500" />
|
|
<select
|
|
value={filter}
|
|
onChange={(e) => setFilter(e.target.value as any)}
|
|
className="text-sm border rounded px-2 py-1"
|
|
>
|
|
<option value="all">All</option>
|
|
<option value="warnings">Warnings</option>
|
|
<option value="recommendations">Recommendations</option>
|
|
<option value="success">Success</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<TabsContent value="insights" className="space-y-4">
|
|
{filteredInsights.length === 0 ? (
|
|
<div className="text-center py-8 text-gray-500">
|
|
<Info className="h-12 w-12 mx-auto mb-4 opacity-50" />
|
|
<p>No insights found for the selected filter</p>
|
|
</div>
|
|
) : (
|
|
<ScrollArea className="h-96">
|
|
<div className="space-y-4">
|
|
{Object.entries(insightsByCategory).map(([category, categoryInsights]) => (
|
|
<div key={category} className="space-y-3">
|
|
<div className="flex items-center gap-2">
|
|
{getCategoryIcon(category)}
|
|
<h3 className="font-medium capitalize">{category}</h3>
|
|
<Badge variant="secondary">{categoryInsights.length}</Badge>
|
|
</div>
|
|
|
|
<div className="space-y-2 pl-6">
|
|
{categoryInsights.map((insight, index) => (
|
|
<div
|
|
key={index}
|
|
className={cn(
|
|
"p-4 rounded-lg border",
|
|
getInsightColor(insight.type)
|
|
)}
|
|
>
|
|
<div className="flex items-start gap-3">
|
|
<div className="mt-0.5">
|
|
{getInsightIcon(insight.type)}
|
|
</div>
|
|
|
|
<div className="flex-1">
|
|
<div className="flex items-center gap-2 mb-2">
|
|
<h4 className="font-medium">{insight.title}</h4>
|
|
{insight.severity && (
|
|
<Badge variant="outline" className={getSeverityColor(insight.severity)}>
|
|
{insight.severity}
|
|
</Badge>
|
|
)}
|
|
{insight.actionable && (
|
|
<Badge variant="outline" className="bg-blue-100 text-blue-800">
|
|
Actionable
|
|
</Badge>
|
|
)}
|
|
</div>
|
|
|
|
<p className="text-sm text-gray-700 mb-2">
|
|
{insight.description}
|
|
</p>
|
|
|
|
{insight.recommendation && (
|
|
<div className="bg-white bg-opacity-50 p-3 rounded border border-gray-200">
|
|
<p className="text-sm font-medium text-gray-800 mb-1">
|
|
Recommendation:
|
|
</p>
|
|
<p className="text-sm text-gray-700">
|
|
{insight.recommendation}
|
|
</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</ScrollArea>
|
|
)}
|
|
</TabsContent>
|
|
|
|
<TabsContent value="patterns" className="space-y-4">
|
|
{llmAnalysis?.patterns && llmAnalysis.patterns.length > 0 ? (
|
|
<ScrollArea className="h-96">
|
|
<div className="space-y-3">
|
|
{llmAnalysis.patterns.map((pattern, index) => (
|
|
<div key={index} className="p-4 border rounded-lg">
|
|
<div className="flex items-center justify-between mb-2">
|
|
<h4 className="font-medium">{pattern.type}</h4>
|
|
<div className="flex items-center gap-2">
|
|
<Badge variant="outline">
|
|
{pattern.frequency} occurrences
|
|
</Badge>
|
|
<Badge
|
|
variant="outline"
|
|
className={getSeverityColor(pattern.impact)}
|
|
>
|
|
{pattern.impact} impact
|
|
</Badge>
|
|
</div>
|
|
</div>
|
|
<p className="text-sm text-gray-700">{pattern.description}</p>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</ScrollArea>
|
|
) : (
|
|
<div className="text-center py-8 text-gray-500">
|
|
<TrendingUp className="h-12 w-12 mx-auto mb-4 opacity-50" />
|
|
<p>No patterns detected yet</p>
|
|
<p className="text-sm">AI analysis will identify recurring work patterns</p>
|
|
</div>
|
|
)}
|
|
</TabsContent>
|
|
|
|
<TabsContent value="recommendations" className="space-y-4">
|
|
{llmAnalysis?.recommendations && llmAnalysis.recommendations.length > 0 ? (
|
|
<ScrollArea className="h-96">
|
|
<div className="space-y-3">
|
|
{llmAnalysis.recommendations.map((rec, index) => (
|
|
<div key={index} className="p-4 border rounded-lg">
|
|
<div className="flex items-center justify-between mb-2">
|
|
<h4 className="font-medium">{rec.category}</h4>
|
|
<div className="flex items-center gap-2">
|
|
<Badge
|
|
variant="outline"
|
|
className={getSeverityColor(rec.priority)}
|
|
>
|
|
{rec.priority} priority
|
|
</Badge>
|
|
</div>
|
|
</div>
|
|
<p className="text-sm text-gray-700 mb-2">{rec.action}</p>
|
|
<div className="text-xs text-gray-600 bg-gray-50 p-2 rounded">
|
|
<strong>Expected Impact:</strong> {rec.expectedImpact}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</ScrollArea>
|
|
) : (
|
|
<div className="text-center py-8 text-gray-500">
|
|
<Lightbulb className="h-12 w-12 mx-auto mb-4 opacity-50" />
|
|
<p>No recommendations available yet</p>
|
|
<p className="text-sm">AI will provide actionable recommendations based on analysis</p>
|
|
</div>
|
|
)}
|
|
</TabsContent>
|
|
|
|
{llmAnalysis && (
|
|
<TabsContent value="llm" className="space-y-4">
|
|
<div className="space-y-6">
|
|
{/* Summary */}
|
|
<div className="p-4 bg-gray-50 rounded-lg">
|
|
<h4 className="font-medium mb-3">AI Summary</h4>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div>
|
|
<div className="text-sm text-gray-600">Overall Quality</div>
|
|
<div className="text-2xl font-bold text-blue-600">
|
|
{Math.round(llmAnalysis.summary.overallQuality * 100)}%
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<div className="text-sm text-gray-600">Productivity Level</div>
|
|
<div className="text-2xl font-bold text-green-600">
|
|
{Math.round(llmAnalysis.summary.productivityLevel * 100)}%
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{llmAnalysis.summary.keyFindings.length > 0 && (
|
|
<div className="mt-4">
|
|
<h5 className="font-medium text-sm mb-2">Key Findings</h5>
|
|
<ul className="text-sm text-gray-700 space-y-1">
|
|
{llmAnalysis.summary.keyFindings.map((finding, index) => (
|
|
<li key={index} className="flex items-start gap-2">
|
|
<span className="text-blue-500 mt-1">•</span>
|
|
{finding}
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Processing Info */}
|
|
<div className="text-xs text-gray-500 border-t pt-4">
|
|
<div className="flex justify-between">
|
|
<span>Processing time: {llmAnalysis.processingTime}ms</span>
|
|
<span>Tokens used: {llmAnalysis.tokensUsed}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</TabsContent>
|
|
)}
|
|
</Tabs>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|