wulf-pulse/app/admin/analytics/time-entries/page.tsx
root 6eee14f8af Add comprehensive admin features and multi-system integration
- 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
2025-11-19 14:18:16 -05:00

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(' ');
}