- 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
544 lines
18 KiB
TypeScript
544 lines
18 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 { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
|
import { Input } from '@/components/ui/input';
|
|
import { Label } from '@/components/ui/label';
|
|
import { Checkbox } from '@/components/ui/checkbox';
|
|
import {
|
|
Calendar,
|
|
Filter,
|
|
Download,
|
|
RefreshCw,
|
|
TrendingUp,
|
|
Clock,
|
|
Users,
|
|
Target,
|
|
Activity,
|
|
BarChart3,
|
|
Settings,
|
|
AlertCircle,
|
|
CheckCircle
|
|
} from 'lucide-react';
|
|
|
|
import { TimelineView } from '@/components/analytics/TimelineView';
|
|
import {
|
|
ScoreCard,
|
|
ActivityScoreCard,
|
|
ContentScoreCard,
|
|
TimelinessScoreCard,
|
|
AggregateScoreCard
|
|
} from '@/components/analytics/ScoreCard';
|
|
import { AnalysisPanel } from '@/components/analytics/AnalysisPanel';
|
|
|
|
import { TimeEntry } from '@/lib/types/database';
|
|
import {
|
|
TimelineEvent,
|
|
AnalyticsInsight,
|
|
LLMAnalysisResponse,
|
|
AggregateAnalysis
|
|
} from '@/lib/types/analytics';
|
|
|
|
export default function TimeEntriesAnalyticsPage() {
|
|
const [loading, setLoading] = useState(true);
|
|
const [timeEntries, setTimeEntries] = useState<TimeEntry[]>([]);
|
|
const [timelineEvents, setTimelineEvents] = useState<TimelineEvent[]>([]);
|
|
const [analysis, setAnalysis] = useState<AggregateAnalysis | null>(null);
|
|
const [insights, setInsights] = useState<AnalyticsInsight[]>([]);
|
|
const [llmAnalysis, setLlmAnalysis] = useState<LLMAnalysisResponse | undefined>(undefined);
|
|
|
|
// Filter states
|
|
const [timeRange, setTimeRange] = useState<'hour' | 'day' | 'week' | 'month'>('day');
|
|
const [selectedResources, setSelectedResources] = useState<number[]>([]);
|
|
const [selectedProjects, setSelectedProjects] = useState<number[]>([]);
|
|
const [selectedTickets, setSelectedTickets] = useState<number[]>([]);
|
|
const [startDate, setStartDate] = useState<string>('');
|
|
const [endDate, setEndDate] = useState<string>('');
|
|
const [minHours, setMinHours] = useState<string>('');
|
|
const [maxHours, setMaxHours] = useState<string>('');
|
|
const [billable, setBillable] = useState<boolean | undefined>(undefined);
|
|
const [approved, setApproved] = useState<boolean | undefined>(undefined);
|
|
|
|
// Mock data for demonstration
|
|
useEffect(() => {
|
|
loadMockData();
|
|
}, []);
|
|
|
|
const loadMockData = async () => {
|
|
setLoading(true);
|
|
|
|
try {
|
|
// Build query parameters
|
|
const params = new URLSearchParams();
|
|
if (startDate) params.append('start_date', startDate);
|
|
if (endDate) params.append('end_date', endDate);
|
|
if (minHours) params.append('min_hours', minHours);
|
|
if (maxHours) params.append('max_hours', maxHours);
|
|
if (billable !== undefined) params.append('billable', String(billable));
|
|
if (approved !== undefined) params.append('approved', String(approved));
|
|
if (selectedTickets.length > 0) params.append('ticket_id', String(selectedTickets[0]));
|
|
params.append('limit', '1000'); // Get more data for analytics
|
|
|
|
// Fetch real time entries from API
|
|
const response = await fetch(`/api/data/time-entries?${params}`);
|
|
if (!response.ok) {
|
|
throw new Error('Failed to fetch time entries');
|
|
}
|
|
|
|
const data = await response.json();
|
|
const fetchedEntries: TimeEntry[] = data.timeEntries || [];
|
|
|
|
// Use fetched entries instead of mock data
|
|
const timeEntriesData = fetchedEntries.length > 0 ? fetchedEntries : [];
|
|
|
|
// Fallback mock time entries if no data
|
|
const mockTimeEntries: TimeEntry[] = timeEntriesData.length > 0 ? timeEntriesData : [
|
|
{
|
|
id: 1,
|
|
resource_id: 1,
|
|
ticket_id: 101,
|
|
task_id: 201,
|
|
project_id: 301,
|
|
company_id: 401,
|
|
entry_date: new Date('2024-01-15T09:00:00Z'),
|
|
hours_worked: 2.5,
|
|
notes: 'Fixed critical bug in authentication system. Updated JWT token validation logic.',
|
|
title: 'Bug Fix - Authentication',
|
|
type: 1,
|
|
start_date_time: new Date('2024-01-15T09:00:00Z'),
|
|
end_date_time: new Date('2024-01-15T11:30:00Z'),
|
|
billable: true,
|
|
approved: true,
|
|
created_at: new Date('2024-01-15T12:00:00Z'),
|
|
updated_at: new Date('2024-01-15T12:00:00Z'),
|
|
synced_at: new Date('2024-01-15T12:00:00Z'),
|
|
is_deleted: false,
|
|
},
|
|
{
|
|
id: 2,
|
|
resource_id: 2,
|
|
ticket_id: 102,
|
|
task_id: 202,
|
|
project_id: 302,
|
|
company_id: 402,
|
|
entry_date: new Date('2024-01-15T14:00:00Z'),
|
|
hours_worked: 4.0,
|
|
notes: 'Implemented new dashboard feature with React components. Added data visualization charts.',
|
|
title: 'Feature Development - Dashboard',
|
|
type: 2,
|
|
start_date_time: new Date('2024-01-15T14:00:00Z'),
|
|
end_date_time: new Date('2024-01-15T18:00:00Z'),
|
|
billable: true,
|
|
approved: false,
|
|
created_at: new Date('2024-01-15T18:30:00Z'),
|
|
updated_at: new Date('2024-01-15T18:30:00Z'),
|
|
synced_at: new Date('2024-01-15T18:30:00Z'),
|
|
is_deleted: false,
|
|
},
|
|
// Add more mock entries as needed
|
|
];
|
|
|
|
// Mock timeline events
|
|
const mockTimelineEvents: TimelineEvent[] = mockTimeEntries.map(entry => ({
|
|
id: `te-${entry.id}`,
|
|
type: 'time_entry',
|
|
timestamp: new Date(entry.entry_date),
|
|
title: entry.title || 'Time Entry',
|
|
description: entry.notes || undefined,
|
|
duration: entry.hours_worked,
|
|
isHumanActivity: true,
|
|
importance: entry.billable ? 'high' : 'medium',
|
|
score: 0.8, // Mock score
|
|
}));
|
|
|
|
// Mock analysis
|
|
const totalHours = mockTimeEntries.reduce((sum, entry) => {
|
|
const hours = typeof entry.hours_worked === 'string' ? parseFloat(entry.hours_worked) : entry.hours_worked;
|
|
return sum + hours;
|
|
}, 0);
|
|
|
|
const mockAnalysis: AggregateAnalysis = {
|
|
totalEntries: mockTimeEntries.length,
|
|
totalHours: totalHours,
|
|
averageHoursPerEntry: mockTimeEntries.length > 0 ? totalHours / mockTimeEntries.length : 0,
|
|
dateRange: {
|
|
earliest: new Date('2024-01-15'),
|
|
latest: new Date('2024-01-15'),
|
|
},
|
|
scores: {
|
|
activity: 0.85,
|
|
content: 0.78,
|
|
timeliness: 0.92,
|
|
overall: 0.85,
|
|
},
|
|
insights: [
|
|
{
|
|
type: 'success',
|
|
category: 'overall',
|
|
title: 'High Quality Time Tracking',
|
|
description: 'Overall time entry quality is excellent.',
|
|
recommendation: 'Maintain current documentation standards.',
|
|
severity: 'low',
|
|
actionable: false,
|
|
},
|
|
{
|
|
type: 'warning',
|
|
category: 'billing',
|
|
title: 'Pending Approvals',
|
|
description: 'Some time entries are awaiting approval.',
|
|
recommendation: 'Review and approve pending time entries.',
|
|
severity: 'medium',
|
|
actionable: true,
|
|
},
|
|
],
|
|
patterns: {
|
|
dayOfWeek: [0, 5, 8, 12, 6, 3, 1],
|
|
hourly: [0, 1, 2, 3, 4, 2, 8, 15, 12, 8, 6, 4, 3, 5, 7, 6, 4, 2, 1, 0, 0, 0, 0, 0],
|
|
},
|
|
trends: {
|
|
weekly: [
|
|
{ week: new Date('2024-01-08'), hours: 25, entries: 8 },
|
|
{ week: new Date('2024-01-15'), hours: 32, entries: 10 },
|
|
],
|
|
},
|
|
analyzedAt: new Date(),
|
|
};
|
|
|
|
// Mock LLM analysis
|
|
const mockLlmAnalysis: LLMAnalysisResponse = {
|
|
insights: [
|
|
'Team shows excellent documentation practices with detailed notes',
|
|
'Consistent time entry patterns indicate good workflow discipline',
|
|
],
|
|
patterns: [
|
|
{
|
|
type: 'Morning Productivity',
|
|
description: 'Most productive work occurs in morning hours (9 AM - 12 PM)',
|
|
frequency: 8,
|
|
impact: 'medium',
|
|
},
|
|
],
|
|
recommendations: [
|
|
{
|
|
category: 'Process Improvement',
|
|
priority: 'medium',
|
|
action: 'Implement automated reminders for time entry approval',
|
|
expectedImpact: 'Reduce approval delays by 50%',
|
|
},
|
|
],
|
|
summary: {
|
|
overallQuality: 0.85,
|
|
productivityLevel: 0.78,
|
|
keyFindings: [
|
|
'Strong documentation quality',
|
|
'Consistent time tracking patterns',
|
|
'Need for faster approval process',
|
|
],
|
|
},
|
|
processingTime: 1250,
|
|
tokensUsed: 245,
|
|
};
|
|
|
|
setTimeEntries(mockTimeEntries);
|
|
setTimelineEvents(mockTimelineEvents);
|
|
setAnalysis(mockAnalysis);
|
|
setInsights(mockAnalysis.insights);
|
|
setLlmAnalysis(mockLlmAnalysis);
|
|
setLoading(false);
|
|
} catch (error) {
|
|
console.error('Error loading time entries:', error);
|
|
setTimeEntries([]);
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleRefresh = () => {
|
|
loadMockData();
|
|
};
|
|
|
|
const handleExport = () => {
|
|
// Implement export functionality
|
|
console.log('Exporting analytics data...');
|
|
};
|
|
|
|
const applyFilters = () => {
|
|
// Implement filter application
|
|
console.log('Applying filters...');
|
|
loadMockData();
|
|
};
|
|
|
|
const clearFilters = () => {
|
|
setSelectedResources([]);
|
|
setSelectedProjects([]);
|
|
setSelectedTickets([]);
|
|
setStartDate('');
|
|
setEndDate('');
|
|
setMinHours('');
|
|
setMaxHours('');
|
|
setBillable(undefined);
|
|
setApproved(undefined);
|
|
loadMockData();
|
|
};
|
|
|
|
return (
|
|
<div className="container mx-auto p-6 space-y-6">
|
|
{/* Header */}
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<h1 className="text-3xl font-bold flex items-center gap-2">
|
|
<BarChart3 className="h-8 w-8 text-blue-600" />
|
|
Time Entries Analytics
|
|
</h1>
|
|
<p className="text-gray-600 mt-2">
|
|
Advanced analytics and insights for time tracking data
|
|
</p>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-2">
|
|
<Button variant="outline" onClick={handleRefresh} disabled={loading}>
|
|
<RefreshCw className={cn("h-4 w-4 mr-2", loading ? "animate-spin" : "")} />
|
|
Refresh
|
|
</Button>
|
|
<Button variant="outline" onClick={handleExport}>
|
|
<Download className="h-4 w-4 mr-2" />
|
|
Export
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Summary Cards */}
|
|
{analysis && (
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
|
<ScoreCard
|
|
title="Total Entries"
|
|
score={analysis.totalEntries / 50} // Normalize to 0-1
|
|
description={`${analysis.totalEntries} time entries`}
|
|
icon={<Activity className="h-5 w-5 text-blue-500" />}
|
|
/>
|
|
<ScoreCard
|
|
title="Total Hours"
|
|
score={Math.min(Number(analysis.totalHours) / 40, 1)} // Normalize to 0-1
|
|
description={`${Number(analysis.totalHours).toFixed(1)} hours tracked`}
|
|
icon={<Clock className="h-5 w-5 text-green-500" />}
|
|
/>
|
|
<ScoreCard
|
|
title="Overall Score"
|
|
score={analysis.scores.overall}
|
|
description="Average quality score"
|
|
icon={<Target className="h-5 w-5 text-purple-500" />}
|
|
/>
|
|
<ScoreCard
|
|
title="Productivity"
|
|
score={analysis.scores.activity}
|
|
description="Activity and consistency score"
|
|
icon={<TrendingUp className="h-5 w-5 text-orange-500" />}
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
|
{/* Filters Panel */}
|
|
<Card className="lg:col-span-1">
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center gap-2">
|
|
<Filter className="h-5 w-5" />
|
|
Filters
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="space-y-4">
|
|
{/* Date Range */}
|
|
<div className="space-y-2">
|
|
<Label>Start Date</Label>
|
|
<Input
|
|
type="date"
|
|
value={startDate}
|
|
onChange={(e) => setStartDate(e.target.value)}
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label>End Date</Label>
|
|
<Input
|
|
type="date"
|
|
value={endDate}
|
|
onChange={(e) => setEndDate(e.target.value)}
|
|
/>
|
|
</div>
|
|
|
|
{/* Ticket ID Filter */}
|
|
<div className="space-y-2">
|
|
<Label>Ticket ID</Label>
|
|
<Input
|
|
type="number"
|
|
placeholder="Enter ticket ID"
|
|
value={selectedTickets[0] || ''}
|
|
onChange={(e) => {
|
|
const ticketId = e.target.value ? parseInt(e.target.value) : null;
|
|
setSelectedTickets(ticketId ? [ticketId] : []);
|
|
}}
|
|
/>
|
|
<p className="text-xs text-muted-foreground">
|
|
Filter timeline to show only entries for this ticket
|
|
</p>
|
|
</div>
|
|
|
|
{/* Hours Range */}
|
|
<div className="grid grid-cols-2 gap-2">
|
|
<div className="space-y-2">
|
|
<Label>Min Hours</Label>
|
|
<Input
|
|
type="number"
|
|
step="0.5"
|
|
placeholder="0"
|
|
value={minHours}
|
|
onChange={(e) => setMinHours(e.target.value)}
|
|
/>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Label>Max Hours</Label>
|
|
<Input
|
|
type="number"
|
|
step="0.5"
|
|
placeholder="24"
|
|
value={maxHours}
|
|
onChange={(e) => setMaxHours(e.target.value)}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Checkboxes */}
|
|
<div className="space-y-2">
|
|
<div className="flex items-center space-x-2">
|
|
<Checkbox
|
|
id="billable"
|
|
checked={billable === true}
|
|
onCheckedChange={(checked) => setBillable(checked === true)}
|
|
/>
|
|
<Label htmlFor="billable">Billable only</Label>
|
|
</div>
|
|
|
|
<div className="flex items-center space-x-2">
|
|
<Checkbox
|
|
id="approved"
|
|
checked={approved === true}
|
|
onCheckedChange={(checked) => setApproved(checked === true)}
|
|
/>
|
|
<Label htmlFor="approved">Approved only</Label>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Action Buttons */}
|
|
<div className="space-y-2 pt-4">
|
|
<Button onClick={applyFilters} className="w-full">
|
|
Apply Filters
|
|
</Button>
|
|
<Button variant="outline" onClick={clearFilters} className="w-full">
|
|
Clear Filters
|
|
</Button>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Main Content */}
|
|
<div className="lg:col-span-2 space-y-6">
|
|
{/* Tabs */}
|
|
<Tabs defaultValue="overview" className="space-y-4">
|
|
<TabsList className="grid w-full grid-cols-4">
|
|
<TabsTrigger value="overview">Overview</TabsTrigger>
|
|
<TabsTrigger value="timeline">Timeline</TabsTrigger>
|
|
<TabsTrigger value="scores">Scores</TabsTrigger>
|
|
<TabsTrigger value="analysis">AI Analysis</TabsTrigger>
|
|
</TabsList>
|
|
|
|
<TabsContent value="overview" className="space-y-4">
|
|
{analysis && <AggregateScoreCard analysis={analysis} />}
|
|
|
|
{/* Additional overview content */}
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>Recent Activity</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<p className="text-gray-600">
|
|
Detailed activity overview and trends will be displayed here.
|
|
</p>
|
|
</CardContent>
|
|
</Card>
|
|
</TabsContent>
|
|
|
|
<TabsContent value="timeline">
|
|
<TimelineView
|
|
events={timelineEvents}
|
|
timeRange={timeRange}
|
|
onTimeRangeChange={setTimeRange}
|
|
loading={loading}
|
|
/>
|
|
</TabsContent>
|
|
|
|
<TabsContent value="scores" className="space-y-4">
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
<ActivityScoreCard
|
|
title="Activity Score"
|
|
score={{
|
|
score: analysis?.scores.activity || 0,
|
|
factors: ['Complete entry details', 'Consistent time tracking'],
|
|
breakdown: {
|
|
completeness: 0.9,
|
|
consistency: 0.8,
|
|
duration: 0.85,
|
|
categorization: 0.9,
|
|
},
|
|
}}
|
|
/>
|
|
<ContentScoreCard
|
|
title="Content Score"
|
|
score={{
|
|
score: analysis?.scores.content || 0,
|
|
factors: ['Detailed work description', 'Technical details included'],
|
|
breakdown: {
|
|
notesQuality: 0.85,
|
|
titleClarity: 0.8,
|
|
internalNotes: 0.7,
|
|
technicalDetail: 0.75,
|
|
},
|
|
}}
|
|
/>
|
|
<TimelinessScoreCard
|
|
title="Timeliness Score"
|
|
score={{
|
|
score: analysis?.scores.timeliness || 0,
|
|
factors: ['Prompt time entry', 'Business hours compliance'],
|
|
breakdown: {
|
|
entryDelay: 0.95,
|
|
businessHours: 0.9,
|
|
regularity: 0.85,
|
|
approvalTimeliness: 0.9,
|
|
},
|
|
}}
|
|
/>
|
|
</div>
|
|
</TabsContent>
|
|
|
|
<TabsContent value="analysis">
|
|
<AnalysisPanel
|
|
insights={insights}
|
|
llmAnalysis={llmAnalysis}
|
|
loading={loading}
|
|
onRefresh={handleRefresh}
|
|
onExport={handleExport}
|
|
/>
|
|
</TabsContent>
|
|
</Tabs>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function cn(...classes: string[]) {
|
|
return classes.filter(Boolean).join(' ');
|
|
}
|