'use client'; import { useState, useEffect, useCallback, useMemo } from 'react'; import { TimeEntry } from '@/lib/types/database'; import { TimelineEvent, AnalyticsInsight, LLMAnalysisResponse, AggregateAnalysis } from '@/lib/types/analytics'; import { analyticsIntegration, EnrichedTimeEntry, EnrichmentOptions } from '@/lib/services/analytics-integration'; import { analyticsEngine } from '@/lib/services/analytics-engine'; import { llmAnalyzer } from '@/lib/services/llm-analyzer'; export interface TimeEntriesFilter { startDate?: string; endDate?: string; resourceIds?: number[]; ticketIds?: number[]; taskIds?: number[]; projectIds?: number[]; companyIds?: number[]; minHours?: number; maxHours?: number; billable?: boolean; approved?: boolean; search?: string; limit?: number; offset?: number; sortBy?: string; sortOrder?: 'asc' | 'desc'; } export interface UseTimeEntriesOptions { autoFetch?: boolean; enableAnalysis?: boolean; enableEnrichment?: boolean; enrichmentOptions?: EnrichmentOptions; cacheKey?: string; cacheTimeout?: number; // milliseconds } export interface TimeEntriesState { timeEntries: TimeEntry[]; enrichedTimeEntries: EnrichedTimeEntry[]; timelineEvents: TimelineEvent[]; analysis: AggregateAnalysis | null; insights: AnalyticsInsight[]; llmAnalysis: LLMAnalysisResponse | null; loading: boolean; error: string | null; pagination: { total: number; limit: number; offset: number; hasMore: boolean; }; } export function useTimeEntries( initialFilter: TimeEntriesFilter = {}, options: UseTimeEntriesOptions = {} ) { const { autoFetch = true, enableAnalysis = true, enableEnrichment = true, enrichmentOptions = { includeResourceInfo: true, includeTicketInfo: true, includeProjectInfo: true, includeCompanyInfo: true, includeAnalysis: true, }, cacheKey = 'time-entries', cacheTimeout = 5 * 60 * 1000, // 5 minutes } = options; const [filter, setFilter] = useState(initialFilter); const [state, setState] = useState({ timeEntries: [], enrichedTimeEntries: [], timelineEvents: [], analysis: null, insights: [], llmAnalysis: null, loading: false, error: null, pagination: { total: 0, limit: 100, offset: 0, hasMore: false, }, }); // Simple cache implementation const cache = useMemo(() => new Map(), []); const getCacheKey = useCallback((filter: TimeEntriesFilter) => { return `${cacheKey}-${JSON.stringify(filter)}`; }, [cacheKey]); const getCachedData = useCallback((key: string) => { const cached = cache.get(key); if (cached && Date.now() - cached.timestamp < cacheTimeout) { return cached.data; } cache.delete(key); return null; }, [cache, cacheTimeout]); const setCachedData = useCallback((key: string, data: any) => { cache.set(key, { data, timestamp: Date.now() }); }, [cache]); // Fetch time entries from API const fetchTimeEntries = useCallback(async (filterOverrides: Partial = {}) => { const currentFilter = { ...filter, ...filterOverrides }; const cacheKey = getCacheKey(currentFilter); // Check cache first const cached = getCachedData(cacheKey); if (cached) { setState(cached); return cached; } setState(prev => ({ ...prev, loading: true, error: null })); try { // Build query string const params = new URLSearchParams(); if (currentFilter.startDate) params.append('start_date', currentFilter.startDate); if (currentFilter.endDate) params.append('end_date', currentFilter.endDate); if (currentFilter.minHours) params.append('min_hours', currentFilter.minHours.toString()); if (currentFilter.maxHours) params.append('max_hours', currentFilter.maxHours.toString()); if (currentFilter.billable !== undefined) params.append('billable', currentFilter.billable.toString()); if (currentFilter.approved !== undefined) params.append('approved', currentFilter.approved.toString()); if (currentFilter.search) params.append('search', currentFilter.search); if (currentFilter.limit) params.append('limit', currentFilter.limit.toString()); if (currentFilter.offset) params.append('offset', currentFilter.offset.toString()); if (currentFilter.sortBy) params.append('sort_by', currentFilter.sortBy); if (currentFilter.sortOrder) params.append('sort_order', currentFilter.sortOrder); if (currentFilter.resourceIds?.length) { params.append('resource_ids', currentFilter.resourceIds.join(',')); } if (currentFilter.ticketIds?.length) { params.append('ticket_ids', currentFilter.ticketIds.join(',')); } if (currentFilter.taskIds?.length) { params.append('task_ids', currentFilter.taskIds.join(',')); } if (currentFilter.projectIds?.length) { params.append('project_ids', currentFilter.projectIds.join(',')); } if (currentFilter.companyIds?.length) { params.append('company_ids', currentFilter.companyIds.join(',')); } const response = await fetch(`/api/data/time-entries?${params}`); if (!response.ok) { throw new Error(`Failed to fetch time entries: ${response.statusText}`); } const data = await response.json(); const timeEntries: TimeEntry[] = data.timeEntries || []; let enrichedTimeEntries: EnrichedTimeEntry[] = []; let timelineEvents: TimelineEvent[] = []; let analysis: AggregateAnalysis | null = null; let insights: AnalyticsInsight[] = []; // Enrich data if enabled if (enableEnrichment && timeEntries.length > 0) { enrichedTimeEntries = await analyticsIntegration.enrichTimeEntries( timeEntries, enrichmentOptions ); timelineEvents = await analyticsIntegration.generateTimelineEvents( timeEntries, enrichmentOptions ); } // Generate analysis if enabled if (enableAnalysis && timeEntries.length > 0) { const analysisResult = await analyticsIntegration.generateComprehensiveAnalysis( timeEntries, enrichmentOptions ); analysis = analysisResult.analysis; insights = analysisResult.entityInsights; } const newState: TimeEntriesState = { timeEntries, enrichedTimeEntries, timelineEvents, analysis, insights, llmAnalysis: null, loading: false, error: null, pagination: data.pagination || { total: timeEntries.length, limit: currentFilter.limit || 100, offset: currentFilter.offset || 0, hasMore: false, }, }; setState(newState); setCachedData(cacheKey, newState); return newState; } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; setState(prev => ({ ...prev, loading: false, error: errorMessage, })); throw error; } }, [filter, enableEnrichment, enableAnalysis, enrichmentOptions, getCacheKey, getCachedData, setCachedData]); // Generate LLM analysis const generateLLMAnalysis = useCallback(async ( analysisType: 'productivity' | 'quality' | 'patterns' | 'anomalies' | 'comprehensive' = 'comprehensive' ) => { if (state.timeEntries.length === 0) { return null; } setState(prev => ({ ...prev, loading: true, error: null })); try { const llmAnalysis = await analyticsIntegration.generateLLMAnalysis( state.timeEntries, analysisType, enrichmentOptions ); setState(prev => ({ ...prev, llmAnalysis, loading: false, })); return llmAnalysis; } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; setState(prev => ({ ...prev, loading: false, error: errorMessage, })); throw error; } }, [state.timeEntries, enrichmentOptions]); // Refresh data const refresh = useCallback(() => { return fetchTimeEntries(); }, [fetchTimeEntries]); // Update filter const updateFilter = useCallback((newFilter: Partial) => { setFilter(prev => ({ ...prev, ...newFilter })); }, []); // Reset filter const resetFilter = useCallback(() => { setFilter(initialFilter); }, [initialFilter]); // Load more data (pagination) const loadMore = useCallback(() => { if (!state.pagination.hasMore) return; return fetchTimeEntries({ offset: state.pagination.offset + state.pagination.limit, }); }, [fetchTimeEntries, state.pagination]); // Export data const exportData = useCallback(async (format: 'csv' | 'excel' | 'pdf' | 'json' = 'csv') => { try { const params = new URLSearchParams({ format }); // Apply current filter to export Object.entries(filter).forEach(([key, value]) => { if (value !== undefined && value !== null) { if (Array.isArray(value)) { params.append(key, value.join(',')); } else { params.append(key, String(value)); } } }); const response = await fetch(`/api/data/time-entries/export?${params}`); if (!response.ok) { throw new Error(`Failed to export data: ${response.statusText}`); } // Get filename from response headers or create default const contentDisposition = response.headers.get('content-disposition'); const filename = contentDisposition ? contentDisposition.split('filename=')[1]?.replace(/"/g, '') : `time-entries-${new Date().toISOString().split('T')[0]}.${format}`; // Download file const blob = await response.blob(); const url = window.URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = filename; document.body.appendChild(a); a.click(); window.URL.revokeObjectURL(url); document.body.removeChild(a); } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; setState(prev => ({ ...prev, error: errorMessage })); throw error; } }, [filter]); // Auto-fetch on filter change useEffect(() => { if (autoFetch) { fetchTimeEntries(); } }, [filter, autoFetch, fetchTimeEntries]); // Memoized computed values const computed = useMemo(() => ({ totalEntries: state.timeEntries.length, totalHours: state.timeEntries.reduce((sum, entry) => sum + entry.hours_worked, 0), averageHoursPerEntry: state.timeEntries.length > 0 ? state.timeEntries.reduce((sum, entry) => sum + entry.hours_worked, 0) / state.timeEntries.length : 0, billableEntries: state.timeEntries.filter(entry => entry.billable).length, approvedEntries: state.timeEntries.filter(entry => entry.approved).length, averageScore: state.analysis?.scores.overall || 0, }), [state.timeEntries, state.analysis]); return { // State ...state, // Computed values computed, // Actions fetchTimeEntries, generateLLMAnalysis, refresh, updateFilter, resetFilter, loadMore, exportData, // Filter filter, setFilter, }; } // Hook for individual time entry analysis export function useTimeEntryAnalysis(timeEntryId: number) { const [timeEntry, setTimeEntry] = useState(null); const [analysis, setAnalysis] = useState(null); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const fetchTimeEntry = useCallback(async () => { setLoading(true); setError(null); try { const response = await fetch(`/api/data/time-entries/${timeEntryId}`); if (!response.ok) { throw new Error(`Failed to fetch time entry: ${response.statusText}`); } const data = await response.json(); setTimeEntry(data.timeEntry); // Generate analysis if (data.timeEntry) { const entryAnalysis = analyticsEngine.analyzeTimeEntry(data.timeEntry); setAnalysis(entryAnalysis); } } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; setError(errorMessage); } finally { setLoading(false); } }, [timeEntryId]); useEffect(() => { if (timeEntryId) { fetchTimeEntry(); } }, [timeEntryId, fetchTimeEntry]); return { timeEntry, analysis, loading, error, refresh: fetchTimeEntry, }; } // Hook for real-time analytics updates export function useRealTimeAnalytics(refreshInterval: number = 60000) { // 1 minute default const [lastUpdate, setLastUpdate] = useState(new Date()); const [isConnected, setIsConnected] = useState(false); const timeEntriesHook = useTimeEntries(); // Set up real-time updates useEffect(() => { const interval = setInterval(() => { timeEntriesHook.refresh(); setLastUpdate(new Date()); }, refreshInterval); setIsConnected(true); return () => { clearInterval(interval); setIsConnected(false); }; }, [refreshInterval, timeEntriesHook]); return { ...timeEntriesHook, lastUpdate, isConnected, }; }