session-wrap-dashboard 3.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/.env.example +6 -0
  2. package/coverage/base.css +224 -0
  3. package/coverage/block-navigation.js +87 -0
  4. package/coverage/coverage-final.json +6 -0
  5. package/coverage/favicon.png +0 -0
  6. package/coverage/index.html +146 -0
  7. package/coverage/prettify.css +1 -0
  8. package/coverage/prettify.js +2 -0
  9. package/coverage/sort-arrow-sprite.png +0 -0
  10. package/coverage/sorter.js +210 -0
  11. package/coverage/src/auth.ts.html +307 -0
  12. package/coverage/src/components/Header.tsx.html +262 -0
  13. package/coverage/src/components/Sidebar.tsx.html +262 -0
  14. package/coverage/src/components/index.html +131 -0
  15. package/coverage/src/hooks/index.html +131 -0
  16. package/coverage/src/hooks/useAuth.ts.html +292 -0
  17. package/coverage/src/hooks/useWorkspace.ts.html +475 -0
  18. package/coverage/src/index.html +116 -0
  19. package/index.html +13 -0
  20. package/package.json +49 -0
  21. package/playwright.config.ts +38 -0
  22. package/postcss.config.js +6 -0
  23. package/src/App.tsx +67 -0
  24. package/src/api.ts +130 -0
  25. package/src/auth.ts +74 -0
  26. package/src/components/AgentLeaderboard.tsx +84 -0
  27. package/src/components/AnalyticsDashboard.tsx +223 -0
  28. package/src/components/Header.tsx +62 -0
  29. package/src/components/IntegrationManager.tsx +292 -0
  30. package/src/components/RoleManager.tsx +230 -0
  31. package/src/components/Sidebar.tsx +62 -0
  32. package/src/components/TrendChart.tsx +92 -0
  33. package/src/components/WorkspaceSelector.tsx +157 -0
  34. package/src/components/__tests__/Header.test.tsx +64 -0
  35. package/src/components/__tests__/Sidebar.test.tsx +39 -0
  36. package/src/components/index.ts +8 -0
  37. package/src/hooks/__tests__/useAuth.test.ts +88 -0
  38. package/src/hooks/__tests__/useWorkspace.test.ts +101 -0
  39. package/src/hooks/index.ts +4 -0
  40. package/src/hooks/useAnalytics.ts +76 -0
  41. package/src/hooks/useApi.ts +62 -0
  42. package/src/hooks/useAuth.ts +69 -0
  43. package/src/hooks/useWorkspace.ts +130 -0
  44. package/src/index.css +57 -0
  45. package/src/main.tsx +13 -0
  46. package/src/pages/DashboardPage.tsx +13 -0
  47. package/src/pages/HomePage.tsx +156 -0
  48. package/src/pages/IntegrationsPage.tsx +9 -0
  49. package/src/pages/RolesPage.tsx +9 -0
  50. package/src/pages/SettingsPage.tsx +118 -0
  51. package/src/pages/WorkspacesPage.tsx +9 -0
  52. package/src/pages/index.ts +6 -0
  53. package/src/test/setup.ts +31 -0
  54. package/src/test/utils.tsx +15 -0
  55. package/src/types.ts +132 -0
  56. package/tailwind.config.js +11 -0
  57. package/tests/e2e/auth.spec.ts +42 -0
  58. package/tests/e2e/dashboard.spec.ts +52 -0
  59. package/tests/e2e/integrations.spec.ts +91 -0
  60. package/tests/e2e/workspaces.spec.ts +78 -0
  61. package/tsconfig.json +26 -0
  62. package/tsconfig.node.json +10 -0
  63. package/vite.config.ts +26 -0
  64. package/vitest.config.ts +31 -0
@@ -0,0 +1,92 @@
1
+ import { memo, useMemo } from 'react'
2
+ import {
3
+ LineChart,
4
+ Line,
5
+ XAxis,
6
+ YAxis,
7
+ CartesianGrid,
8
+ Tooltip,
9
+ Legend,
10
+ ResponsiveContainer
11
+ } from 'recharts'
12
+ import { AnalyticsTrend } from '../types'
13
+
14
+ interface TrendChartProps {
15
+ data: AnalyticsTrend[]
16
+ }
17
+
18
+ const TrendChartComponent = ({ data }: TrendChartProps) => {
19
+ if (!data || data.length === 0) {
20
+ return (
21
+ <div className="flex items-center justify-center h-80">
22
+ <p className="text-slate-500">No trend data available</p>
23
+ </div>
24
+ )
25
+ }
26
+
27
+ // Memoize chart data transformation
28
+ const chartData = useMemo(() =>
29
+ data.map((item) => ({
30
+ date: new Date(item.snapshot_date).toLocaleDateString('en-US', {
31
+ month: 'short',
32
+ day: 'numeric'
33
+ }),
34
+ completed: item.completed_tasks,
35
+ pending: item.pending_tasks,
36
+ inProgress: item.in_progress_tasks,
37
+ decisions: item.total_decisions,
38
+ quality: (item.avg_decision_quality * 100).toFixed(0) // Scale to 0-100 for visibility
39
+ })), [data])
40
+
41
+ return (
42
+ <ResponsiveContainer width="100%" height={300}>
43
+ <LineChart data={chartData}>
44
+ <CartesianGrid strokeDasharray="3 3" stroke="#e2e8f0" />
45
+ <XAxis dataKey="date" stroke="#64748b" />
46
+ <YAxis stroke="#64748b" />
47
+ <Tooltip
48
+ contentStyle={{
49
+ backgroundColor: '#fff',
50
+ border: '1px solid #e2e8f0',
51
+ borderRadius: '0.5rem'
52
+ }}
53
+ />
54
+ <Legend />
55
+ <Line
56
+ type="monotone"
57
+ dataKey="completed"
58
+ stroke="#10b981"
59
+ strokeWidth={2}
60
+ name="Completed"
61
+ connectNulls
62
+ />
63
+ <Line
64
+ type="monotone"
65
+ dataKey="inProgress"
66
+ stroke="#3b82f6"
67
+ strokeWidth={2}
68
+ name="In Progress"
69
+ connectNulls
70
+ />
71
+ <Line
72
+ type="monotone"
73
+ dataKey="pending"
74
+ stroke="#f59e0b"
75
+ strokeWidth={2}
76
+ name="Pending"
77
+ connectNulls
78
+ />
79
+ <Line
80
+ type="monotone"
81
+ dataKey="quality"
82
+ stroke="#8b5cf6"
83
+ strokeWidth={2}
84
+ name="Decision Quality (%)"
85
+ connectNulls
86
+ />
87
+ </LineChart>
88
+ </ResponsiveContainer>
89
+ )
90
+ }
91
+
92
+ export const TrendChart = memo(TrendChartComponent)
@@ -0,0 +1,157 @@
1
+ import { useState } from 'react'
2
+ import { useWorkspace } from '../hooks'
3
+ import { Plus, Lock, Globe } from 'lucide-react'
4
+
5
+ export const WorkspaceSelector = () => {
6
+ const { workspaces, currentWorkspace, setCurrentWorkspace, createWorkspace, isLoading } =
7
+ useWorkspace()
8
+ const [showCreateForm, setShowCreateForm] = useState(false)
9
+ const [formData, setFormData] = useState({ name: '', isPublic: false })
10
+ const [isCreating, setIsCreating] = useState(false)
11
+ const [error, setError] = useState<string | null>(null)
12
+
13
+ const handleCreate = async (e: React.FormEvent) => {
14
+ e.preventDefault()
15
+ if (!formData.name.trim()) {
16
+ setError('Workspace name is required')
17
+ return
18
+ }
19
+
20
+ try {
21
+ setIsCreating(true)
22
+ setError(null)
23
+ await createWorkspace(formData.name, formData.isPublic)
24
+ setFormData({ name: '', isPublic: false })
25
+ setShowCreateForm(false)
26
+ } catch (err) {
27
+ setError(err instanceof Error ? err.message : 'Failed to create workspace')
28
+ } finally {
29
+ setIsCreating(false)
30
+ }
31
+ }
32
+
33
+ if (isLoading) {
34
+ return (
35
+ <div className="flex items-center justify-center h-64">
36
+ <div className="loading-spinner"></div>
37
+ </div>
38
+ )
39
+ }
40
+
41
+ return (
42
+ <div className="space-y-6">
43
+ <div>
44
+ <h1 className="text-2xl font-bold text-slate-900">Workspaces</h1>
45
+ <p className="text-slate-600 mt-2">Manage your workspaces and collaboration spaces</p>
46
+ </div>
47
+
48
+ {/* Workspaces Grid */}
49
+ <div className="grid grid-cols-3 gap-4">
50
+ {workspaces.map((workspace) => (
51
+ <div
52
+ key={workspace.id}
53
+ onClick={() => setCurrentWorkspace(workspace)}
54
+ className={`card cursor-pointer transition-all ${
55
+ currentWorkspace?.id === workspace.id
56
+ ? 'border-blue-500 bg-blue-50'
57
+ : 'hover:border-blue-300'
58
+ }`}
59
+ >
60
+ <div className="flex items-start justify-between mb-3">
61
+ <h3 className="text-lg font-semibold text-slate-900">{workspace.name}</h3>
62
+ <div className="text-slate-500">
63
+ {workspace.is_public ? (
64
+ <Globe size={18} />
65
+ ) : (
66
+ <Lock size={18} />
67
+ )}
68
+ </div>
69
+ </div>
70
+ <p className="text-sm text-slate-600">
71
+ {workspace.is_public ? 'Public' : 'Private'} workspace
72
+ </p>
73
+ {workspace.roles && (
74
+ <div className="mt-3 pt-3 border-t border-slate-200">
75
+ <p className="text-xs text-slate-500">
76
+ {workspace.roles.length} role{workspace.roles.length !== 1 ? 's' : ''}
77
+ </p>
78
+ </div>
79
+ )}
80
+ </div>
81
+ ))}
82
+
83
+ {/* Create New Workspace Card */}
84
+ <button
85
+ onClick={() => setShowCreateForm(true)}
86
+ className="card border-2 border-dashed border-slate-300 hover:border-blue-400 flex flex-col items-center justify-center gap-2 cursor-pointer transition-colors"
87
+ >
88
+ <Plus size={32} className="text-slate-400" />
89
+ <span className="text-slate-600 font-medium">Create New</span>
90
+ </button>
91
+ </div>
92
+
93
+ {/* Create Form Modal */}
94
+ {showCreateForm && (
95
+ <div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
96
+ <div className="bg-white rounded-lg p-6 max-w-md w-full mx-4">
97
+ <h2 className="text-xl font-bold mb-4">Create Workspace</h2>
98
+
99
+ {error && (
100
+ <div className="bg-red-50 border border-red-200 text-red-700 px-4 py-2 rounded mb-4">
101
+ {error}
102
+ </div>
103
+ )}
104
+
105
+ <form onSubmit={handleCreate} className="space-y-4">
106
+ <div>
107
+ <label className="block text-sm font-medium text-slate-700 mb-2">
108
+ Workspace Name
109
+ </label>
110
+ <input
111
+ type="text"
112
+ value={formData.name}
113
+ onChange={(e) => setFormData({ ...formData, name: e.target.value })}
114
+ className="input w-full"
115
+ placeholder="e.g., Engineering Team"
116
+ disabled={isCreating}
117
+ />
118
+ </div>
119
+
120
+ <div className="flex items-center gap-2">
121
+ <input
122
+ type="checkbox"
123
+ id="isPublic"
124
+ checked={formData.isPublic}
125
+ onChange={(e) => setFormData({ ...formData, isPublic: e.target.checked })}
126
+ className="cursor-pointer"
127
+ disabled={isCreating}
128
+ />
129
+ <label htmlFor="isPublic" className="text-sm text-slate-700 cursor-pointer">
130
+ Make this workspace public
131
+ </label>
132
+ </div>
133
+
134
+ <div className="flex gap-3 pt-4">
135
+ <button
136
+ type="submit"
137
+ disabled={isCreating}
138
+ className="btn btn-primary flex-1"
139
+ >
140
+ {isCreating ? 'Creating...' : 'Create'}
141
+ </button>
142
+ <button
143
+ type="button"
144
+ onClick={() => setShowCreateForm(false)}
145
+ disabled={isCreating}
146
+ className="btn btn-secondary flex-1"
147
+ >
148
+ Cancel
149
+ </button>
150
+ </div>
151
+ </form>
152
+ </div>
153
+ </div>
154
+ )}
155
+ </div>
156
+ )
157
+ }
@@ -0,0 +1,64 @@
1
+ import { describe, it, expect, vi } from 'vitest'
2
+ import { screen } from '@testing-library/react'
3
+ import { render } from '../../test/utils'
4
+ import { Header } from '../Header'
5
+
6
+ vi.mock('../../hooks', () => ({
7
+ useAuth: () => ({
8
+ user: {
9
+ id: '1',
10
+ github_login: 'testuser',
11
+ email: 'test@example.com',
12
+ created_at: '2026-01-01'
13
+ },
14
+ logout: vi.fn()
15
+ }),
16
+ useWorkspace: () => ({
17
+ currentWorkspace: {
18
+ id: '1',
19
+ name: 'Test Workspace',
20
+ owner_id: 'user1',
21
+ is_public: false,
22
+ created_at: '2026-01-01',
23
+ updated_at: '2026-01-01'
24
+ },
25
+ workspaces: [
26
+ {
27
+ id: '1',
28
+ name: 'Test Workspace',
29
+ owner_id: 'user1',
30
+ is_public: false,
31
+ created_at: '2026-01-01',
32
+ updated_at: '2026-01-01'
33
+ }
34
+ ],
35
+ setCurrentWorkspace: vi.fn()
36
+ })
37
+ }))
38
+
39
+ describe('Header Component', () => {
40
+ it('should render header with title', () => {
41
+ render(<Header title="Test Dashboard" />)
42
+
43
+ expect(screen.getByText('Test Dashboard')).toBeInTheDocument()
44
+ })
45
+
46
+ it('should display user login name', () => {
47
+ render(<Header />)
48
+
49
+ expect(screen.getByText('testuser')).toBeInTheDocument()
50
+ })
51
+
52
+ it('should have logout button', () => {
53
+ render(<Header />)
54
+
55
+ expect(screen.getByText('Logout')).toBeInTheDocument()
56
+ })
57
+
58
+ it('should show workspace selector', () => {
59
+ render(<Header />)
60
+
61
+ const select = screen.getByDisplayValue('Test Workspace')
62
+ expect(select).toBeInTheDocument()
63
+ })
64
+ })
@@ -0,0 +1,39 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { screen } from '@testing-library/react'
3
+ import { render } from '../../test/utils'
4
+ import { Sidebar } from '../Sidebar'
5
+
6
+ describe('Sidebar Component', () => {
7
+ it('should render sidebar logo', () => {
8
+ render(<Sidebar />)
9
+
10
+ expect(screen.getByText(/Session Wrap/i)).toBeInTheDocument()
11
+ })
12
+
13
+ it('should render navigation items', () => {
14
+ render(<Sidebar />)
15
+
16
+ expect(screen.getByText('Home')).toBeInTheDocument()
17
+ expect(screen.getByText('Analytics')).toBeInTheDocument()
18
+ expect(screen.getByText('Workspaces')).toBeInTheDocument()
19
+ expect(screen.getByText('Roles & Members')).toBeInTheDocument()
20
+ expect(screen.getByText('Integrations')).toBeInTheDocument()
21
+ expect(screen.getByText('Settings')).toBeInTheDocument()
22
+ })
23
+
24
+ it('should have correct navigation links', () => {
25
+ render(<Sidebar />)
26
+
27
+ const homeLink = screen.getByRole('link', { name: /Home/i })
28
+ expect(homeLink).toHaveAttribute('href', '/')
29
+
30
+ const dashboardLink = screen.getByRole('link', { name: /Analytics/i })
31
+ expect(dashboardLink).toHaveAttribute('href', '/dashboard')
32
+ })
33
+
34
+ it('should display version', () => {
35
+ render(<Sidebar />)
36
+
37
+ expect(screen.getByText('v3.9.0')).toBeInTheDocument()
38
+ })
39
+ })
@@ -0,0 +1,8 @@
1
+ export { Header } from './Header'
2
+ export { Sidebar } from './Sidebar'
3
+ export { AnalyticsDashboard } from './AnalyticsDashboard'
4
+ export { TrendChart } from './TrendChart'
5
+ export { AgentLeaderboard } from './AgentLeaderboard'
6
+ export { WorkspaceSelector } from './WorkspaceSelector'
7
+ export { RoleManager } from './RoleManager'
8
+ export { IntegrationManager } from './IntegrationManager'
@@ -0,0 +1,88 @@
1
+ import { describe, it, expect, beforeEach, vi } from 'vitest'
2
+ import { renderHook, act } from '@testing-library/react'
3
+ import { useAuth } from '../useAuth'
4
+ import * as authService from '../../auth'
5
+
6
+ vi.mock('../../auth')
7
+ vi.mock('../../api', () => ({
8
+ userAPI: {
9
+ getProfile: vi.fn(() =>
10
+ Promise.resolve({
11
+ data: {
12
+ user: {
13
+ id: '1',
14
+ github_login: 'testuser',
15
+ email: 'test@example.com',
16
+ created_at: '2026-01-01'
17
+ }
18
+ }
19
+ })
20
+ )
21
+ }
22
+ }))
23
+
24
+ describe('useAuth', () => {
25
+ beforeEach(() => {
26
+ vi.clearAllMocks()
27
+ ;(authService.initAuth as any).mockResolvedValue({
28
+ token: null,
29
+ isAuthenticated: false
30
+ })
31
+ })
32
+
33
+ it('should initialize with loading state', () => {
34
+ const { result } = renderHook(() => useAuth())
35
+
36
+ expect(result.current.isLoading).toBe(true)
37
+ expect(result.current.isAuthenticated).toBe(false)
38
+ expect(result.current.user).toBe(null)
39
+ })
40
+
41
+ it('should handle login', async () => {
42
+ ;(authService.login as any).mockResolvedValue({
43
+ user: {
44
+ id: '1',
45
+ github_login: 'testuser',
46
+ email: 'test@example.com',
47
+ created_at: '2026-01-01'
48
+ }
49
+ })
50
+
51
+ const { result } = renderHook(() => useAuth())
52
+
53
+ await act(async () => {
54
+ await result.current.login('test-token')
55
+ })
56
+
57
+ expect(result.current.isAuthenticated).toBe(true)
58
+ expect(result.current.user?.github_login).toBe('testuser')
59
+ })
60
+
61
+ it('should handle logout', async () => {
62
+ ;(authService.logout as any).mockImplementation(() => {})
63
+
64
+ const { result } = renderHook(() => useAuth())
65
+
66
+ await act(async () => {
67
+ result.current.logout()
68
+ })
69
+
70
+ expect(authService.logout).toHaveBeenCalled()
71
+ expect(result.current.isAuthenticated).toBe(false)
72
+ expect(result.current.user).toBe(null)
73
+ })
74
+
75
+ it('should catch login errors', async () => {
76
+ ;(authService.login as any).mockRejectedValue(
77
+ new Error('Auth failed')
78
+ )
79
+
80
+ const { result } = renderHook(() => useAuth())
81
+
82
+ await expect(
83
+ act(async () => {
84
+ await result.current.login('invalid-token')
85
+ })
86
+ ).rejects.toThrow('Auth failed')
87
+ })
88
+ })
@@ -0,0 +1,101 @@
1
+ import { describe, it, expect, beforeEach, vi } from 'vitest'
2
+ import { renderHook, act } from '@testing-library/react'
3
+ import { useWorkspace } from '../useWorkspace'
4
+
5
+ vi.mock('../../api', () => ({
6
+ workspaceAPI: {
7
+ list: vi.fn(() =>
8
+ Promise.resolve({
9
+ data: [
10
+ {
11
+ id: '1',
12
+ name: 'Test Workspace',
13
+ owner_id: 'user1',
14
+ is_public: false,
15
+ created_at: '2026-01-01',
16
+ updated_at: '2026-01-01',
17
+ roles: ['admin']
18
+ }
19
+ ]
20
+ })
21
+ ),
22
+ getMembers: vi.fn(() =>
23
+ Promise.resolve({
24
+ data: [
25
+ {
26
+ id: 'member1',
27
+ github_login: 'testuser',
28
+ email: 'test@example.com',
29
+ avatar_url: null,
30
+ roles: ['admin']
31
+ }
32
+ ]
33
+ })
34
+ ),
35
+ addMember: vi.fn(() => Promise.resolve({})),
36
+ updateMember: vi.fn(() => Promise.resolve({})),
37
+ removeMember: vi.fn(() => Promise.resolve({}))
38
+ },
39
+ rbacAPI: {
40
+ getRoles: vi.fn(() =>
41
+ Promise.resolve({
42
+ data: [
43
+ {
44
+ id: '1',
45
+ name: 'admin',
46
+ description: 'Administrator',
47
+ permissions: {}
48
+ }
49
+ ]
50
+ })
51
+ )
52
+ }
53
+ }))
54
+
55
+ describe('useWorkspace', () => {
56
+ beforeEach(() => {
57
+ vi.clearAllMocks()
58
+ })
59
+
60
+ it('should load workspaces on mount', async () => {
61
+ const { result } = renderHook(() => useWorkspace())
62
+
63
+ expect(result.current.isLoading).toBe(true)
64
+
65
+ // Wait for initial load
66
+ await new Promise((resolve) => setTimeout(resolve, 100))
67
+
68
+ expect(result.current.workspaces.length).toBe(1)
69
+ expect(result.current.currentWorkspace?.name).toBe('Test Workspace')
70
+ })
71
+
72
+ it('should set current workspace', async () => {
73
+ const { result } = renderHook(() => useWorkspace())
74
+
75
+ await new Promise((resolve) => setTimeout(resolve, 100))
76
+
77
+ const workspace = result.current.workspaces[0]
78
+
79
+ await act(async () => {
80
+ result.current.setCurrentWorkspace(workspace)
81
+ })
82
+
83
+ expect(result.current.currentWorkspace).toBe(workspace)
84
+ })
85
+
86
+ it('should load members for current workspace', async () => {
87
+ const { result } = renderHook(() => useWorkspace())
88
+
89
+ await new Promise((resolve) => setTimeout(resolve, 100))
90
+
91
+ expect(result.current.members.length).toBe(1)
92
+ expect(result.current.members[0].github_login).toBe('testuser')
93
+ })
94
+
95
+ it('should handle errors gracefully', () => {
96
+ const { result } = renderHook(() => useWorkspace())
97
+
98
+ // Should not throw
99
+ expect(result.current.error).toBe(null)
100
+ })
101
+ })
@@ -0,0 +1,4 @@
1
+ export { useAuth } from './useAuth'
2
+ export { useWorkspace } from './useWorkspace'
3
+ export { useAnalytics } from './useAnalytics'
4
+ export { useApi } from './useApi'
@@ -0,0 +1,76 @@
1
+ import { useState, useEffect, useCallback } from 'react'
2
+ import { analyticsAPI } from '../api'
3
+ import * as types from '../types'
4
+
5
+ interface UseAnalyticsReturn {
6
+ dashboard: types.AnalyticsDashboard | null
7
+ insights: types.Insight[]
8
+ trends: types.AnalyticsTrend[]
9
+ agents: types.AgentPerformance[]
10
+ isLoading: boolean
11
+ error: string | null
12
+ days: number
13
+ setDays: (days: number) => void
14
+ refresh: () => Promise<void>
15
+ }
16
+
17
+ export const useAnalytics = (workspaceId: string | null): UseAnalyticsReturn => {
18
+ const [dashboard, setDashboard] = useState<types.AnalyticsDashboard | null>(null)
19
+ const [insights, setInsights] = useState<types.Insight[]>([])
20
+ const [trends, setTrends] = useState<types.AnalyticsTrend[]>([])
21
+ const [agents, setAgents] = useState<types.AgentPerformance[]>([])
22
+ const [isLoading, setIsLoading] = useState(false)
23
+ const [error, setError] = useState<string | null>(null)
24
+ const [days, setDays] = useState(30)
25
+
26
+ const loadData = useCallback(async () => {
27
+ if (!workspaceId) {
28
+ setDashboard(null)
29
+ setInsights([])
30
+ setTrends([])
31
+ setAgents([])
32
+ return
33
+ }
34
+
35
+ try {
36
+ setIsLoading(true)
37
+ setError(null)
38
+
39
+ const [dashboardRes, insightsRes, agentsRes] = await Promise.all([
40
+ analyticsAPI.getDashboard(workspaceId, days),
41
+ analyticsAPI.getInsights(workspaceId, days),
42
+ analyticsAPI.getAgents(workspaceId, days)
43
+ ])
44
+
45
+ setDashboard(dashboardRes.data)
46
+ setInsights(insightsRes.data.insights)
47
+ setAgents(agentsRes.data.agents)
48
+ setTrends(dashboardRes.data.trends)
49
+ } catch (err) {
50
+ setError(err instanceof Error ? err.message : 'Failed to load analytics')
51
+ } finally {
52
+ setIsLoading(false)
53
+ }
54
+ }, [workspaceId, days])
55
+
56
+ // Load data when workspace or days changes
57
+ useEffect(() => {
58
+ loadData()
59
+ }, [loadData])
60
+
61
+ const refresh = useCallback(async () => {
62
+ await loadData()
63
+ }, [loadData])
64
+
65
+ return {
66
+ dashboard,
67
+ insights,
68
+ trends,
69
+ agents,
70
+ isLoading,
71
+ error,
72
+ days,
73
+ setDays,
74
+ refresh
75
+ }
76
+ }
@@ -0,0 +1,62 @@
1
+ import { useState, useCallback } from 'react'
2
+
3
+ interface UseApiOptions {
4
+ onSuccess?: (data: any) => void
5
+ onError?: (error: Error) => void
6
+ }
7
+
8
+ interface UseApiReturn<T> {
9
+ data: T | null
10
+ isLoading: boolean
11
+ error: Error | null
12
+ execute: (...args: any[]) => Promise<T>
13
+ reset: () => void
14
+ }
15
+
16
+ export const useApi = <T,>(
17
+ apiFunction: (...args: any[]) => Promise<any>,
18
+ options?: UseApiOptions
19
+ ): UseApiReturn<T> => {
20
+ const [data, setData] = useState<T | null>(null)
21
+ const [isLoading, setIsLoading] = useState(false)
22
+ const [error, setError] = useState<Error | null>(null)
23
+
24
+ const execute = useCallback(
25
+ async (...args: any[]): Promise<T> => {
26
+ try {
27
+ setIsLoading(true)
28
+ setError(null)
29
+
30
+ const response = await apiFunction(...args)
31
+ const result = response.data
32
+
33
+ setData(result)
34
+ options?.onSuccess?.(result)
35
+
36
+ return result
37
+ } catch (err) {
38
+ const error = err instanceof Error ? err : new Error(String(err))
39
+ setError(error)
40
+ options?.onError?.(error)
41
+ throw error
42
+ } finally {
43
+ setIsLoading(false)
44
+ }
45
+ },
46
+ [apiFunction, options]
47
+ )
48
+
49
+ const reset = useCallback(() => {
50
+ setData(null)
51
+ setError(null)
52
+ setIsLoading(false)
53
+ }, [])
54
+
55
+ return {
56
+ data,
57
+ isLoading,
58
+ error,
59
+ execute,
60
+ reset
61
+ }
62
+ }