@burdenoff/microfe-timecampus 2026.731.2 → 2026.820.1

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 (159) hide show
  1. package/dist/generated/wspace-operations.d.ts +484 -861
  2. package/dist/generated/wspace-operations.d.ts.map +1 -1
  3. package/dist/generated/wspace-operations.js +1377 -1377
  4. package/dist/generated/wspace-operations.js.map +1 -1
  5. package/dist/generated/wspace-types.js.map +1 -1
  6. package/dist/index.js +5 -3
  7. package/dist/services/createSandboxAssistantTransport.js.map +1 -1
  8. package/dist/stories/EmptyState.stories.d.ts +0 -1
  9. package/dist/stories/LoadingSkeleton.stories.d.ts +0 -1
  10. package/dist/stories/StatCard.stories.d.ts +0 -1
  11. package/dist/stories/StatusBadge.stories.d.ts +0 -1
  12. package/dist/timecampus/AdminRoot.d.ts +8 -0
  13. package/dist/timecampus/AdminRoot.d.ts.map +1 -0
  14. package/dist/timecampus/AdminRoot.js +12 -0
  15. package/dist/timecampus/AdminRoot.js.map +1 -0
  16. package/dist/timecampus/AdminRoutes.d.ts +3 -0
  17. package/dist/timecampus/AdminRoutes.d.ts.map +1 -0
  18. package/dist/timecampus/AdminRoutes.js +8 -0
  19. package/dist/timecampus/AdminRoutes.js.map +1 -0
  20. package/dist/timecampus/TimeCampusRoot.d.ts +0 -1
  21. package/dist/timecampus/TimeCampusRoot.js +4 -2
  22. package/dist/timecampus/TimeCampusRoot.js.map +1 -1
  23. package/dist/timecampus/TimeCampusRoutes.js +1 -1
  24. package/dist/timecampus/TimeCampusRoutes.js.map +1 -1
  25. package/dist/timecampus/components/ChartCard.d.ts +0 -1
  26. package/dist/timecampus/components/ChartCard.js.map +1 -1
  27. package/dist/timecampus/components/DataTable.d.ts +0 -1
  28. package/dist/timecampus/components/EmptyState.d.ts +0 -1
  29. package/dist/timecampus/components/EmptyState.js.map +1 -1
  30. package/dist/timecampus/components/ErrorBoundary.d.ts +0 -1
  31. package/dist/timecampus/components/ErrorBoundary.js.map +1 -1
  32. package/dist/timecampus/components/FilterBar.d.ts +0 -1
  33. package/dist/timecampus/components/LoadingSkeleton.d.ts +0 -1
  34. package/dist/timecampus/components/LoadingSkeleton.js.map +1 -1
  35. package/dist/timecampus/components/Modal.d.ts +0 -1
  36. package/dist/timecampus/components/Modal.js +5 -3
  37. package/dist/timecampus/components/Modal.js.map +1 -1
  38. package/dist/timecampus/components/PageLayout.d.ts +0 -1
  39. package/dist/timecampus/components/PageLayout.js.map +1 -1
  40. package/dist/timecampus/components/StatCard.d.ts +0 -1
  41. package/dist/timecampus/components/StatCard.js.map +1 -1
  42. package/dist/timecampus/components/StatusBadge.d.ts +0 -1
  43. package/dist/timecampus/components/StatusBadge.js.map +1 -1
  44. package/dist/timecampus/components/Toast.d.ts +0 -1
  45. package/dist/timecampus/components/Toast.js.map +1 -1
  46. package/dist/timecampus/components/charts/BalanceWheel.js.map +1 -1
  47. package/dist/timecampus/components/charts/BarsChart.d.ts +0 -1
  48. package/dist/timecampus/components/charts/BarsChart.js.map +1 -1
  49. package/dist/timecampus/components/charts/ChartParts.d.ts +0 -1
  50. package/dist/timecampus/components/charts/ChartParts.js.map +1 -1
  51. package/dist/timecampus/components/charts/DonutChart.d.ts +0 -1
  52. package/dist/timecampus/components/charts/DonutChart.js.map +1 -1
  53. package/dist/timecampus/components/charts/Heatmap.d.ts +0 -1
  54. package/dist/timecampus/components/charts/Heatmap.js +29 -29
  55. package/dist/timecampus/components/charts/Heatmap.js.map +1 -1
  56. package/dist/timecampus/components/charts/ProgressMeter.d.ts +0 -1
  57. package/dist/timecampus/components/charts/ProgressMeter.js +2 -2
  58. package/dist/timecampus/components/charts/ProgressMeter.js.map +1 -1
  59. package/dist/timecampus/components/charts/RadialGauge.d.ts +0 -1
  60. package/dist/timecampus/components/charts/RadialGauge.js +2 -2
  61. package/dist/timecampus/components/charts/RadialGauge.js.map +1 -1
  62. package/dist/timecampus/components/charts/TrendChart.d.ts +0 -1
  63. package/dist/timecampus/components/charts/TrendChart.js.map +1 -1
  64. package/dist/timecampus/components/charts/WeeksGrid.js +2 -2
  65. package/dist/timecampus/components/charts/WeeksGrid.js.map +1 -1
  66. package/dist/timecampus/components/charts/chartTheme.js.map +1 -1
  67. package/dist/timecampus/components/charts/index.js +10 -9
  68. package/dist/timecampus/components/index.js +18 -17
  69. package/dist/timecampus/index.d.ts +3 -1
  70. package/dist/timecampus/index.d.ts.map +1 -1
  71. package/dist/timecampus/index.js +8 -5
  72. package/dist/timecampus/pages/HomePage.d.ts +0 -1
  73. package/dist/timecampus/pages/HomePage.js.map +1 -1
  74. package/dist/timecampus/pages/admin/AdminPage.d.ts +3 -0
  75. package/dist/timecampus/pages/admin/AdminPage.d.ts.map +1 -0
  76. package/dist/timecampus/pages/admin/AdminPage.js +235 -0
  77. package/dist/timecampus/pages/admin/AdminPage.js.map +1 -0
  78. package/dist/timecampus/pages/application-usage/ApplicationUsagePage.d.ts.map +1 -1
  79. package/dist/timecampus/pages/application-usage/ApplicationUsagePage.js.map +1 -1
  80. package/dist/timecampus/pages/client-portal/ClientPortalPage.d.ts.map +1 -1
  81. package/dist/timecampus/pages/client-portal/ClientPortalPage.js.map +1 -1
  82. package/dist/timecampus/pages/dashboard/AnalyticsPage.js.map +1 -1
  83. package/dist/timecampus/pages/dashboard/ReportsPage.js.map +1 -1
  84. package/dist/timecampus/pages/email-tracking/EmailAnalyticsPage.js.map +1 -1
  85. package/dist/timecampus/pages/email-tracking/EmailTrackingPage.d.ts.map +1 -1
  86. package/dist/timecampus/pages/email-tracking/EmailTrackingPage.js.map +1 -1
  87. package/dist/timecampus/pages/expense-tracking/ExpenseTrackingPage.d.ts.map +1 -1
  88. package/dist/timecampus/pages/expense-tracking/ExpenseTrackingPage.js.map +1 -1
  89. package/dist/timecampus/pages/focus-score/FocusScorePage.js.map +1 -1
  90. package/dist/timecampus/pages/geofencing/GeofencingPage.d.ts.map +1 -1
  91. package/dist/timecampus/pages/geofencing/GeofencingPage.js.map +1 -1
  92. package/dist/timecampus/pages/invoicing/InvoicingPage.d.ts.map +1 -1
  93. package/dist/timecampus/pages/invoicing/InvoicingPage.js.map +1 -1
  94. package/dist/timecampus/pages/lifetime/DeadTimeRadarPage.js.map +1 -1
  95. package/dist/timecampus/pages/lifetime/FiniteLifePage.js.map +1 -1
  96. package/dist/timecampus/pages/lifetime/LifeBalancePage.js.map +1 -1
  97. package/dist/timecampus/pages/lifetime/LifeTimelinePage.js.map +1 -1
  98. package/dist/timecampus/pages/lifetime/PersonalCorporatePage.js.map +1 -1
  99. package/dist/timecampus/pages/lifetime/RecommendationsPage.js +1 -1
  100. package/dist/timecampus/pages/lifetime/RecommendationsPage.js.map +1 -1
  101. package/dist/timecampus/pages/lifetime/TimeWellSpentPage.js.map +1 -1
  102. package/dist/timecampus/pages/lifetime/ValuesGoalsPage.js +9 -6
  103. package/dist/timecampus/pages/lifetime/ValuesGoalsPage.js.map +1 -1
  104. package/dist/timecampus/pages/lifetime/domains.d.ts +0 -1
  105. package/dist/timecampus/pages/lifetime/domains.js +1 -1
  106. package/dist/timecampus/pages/lifetime/domains.js.map +1 -1
  107. package/dist/timecampus/pages/location-tracking/LocationTrackingPage.d.ts.map +1 -1
  108. package/dist/timecampus/pages/location-tracking/LocationTrackingPage.js.map +1 -1
  109. package/dist/timecampus/pages/meetings/ActionItemFormModal.d.ts +0 -1
  110. package/dist/timecampus/pages/meetings/ActionItemFormModal.js.map +1 -1
  111. package/dist/timecampus/pages/meetings/MeetingAnalyticsPage.js +1 -1
  112. package/dist/timecampus/pages/meetings/MeetingAnalyticsPage.js.map +1 -1
  113. package/dist/timecampus/pages/meetings/MeetingCreatePage.js.map +1 -1
  114. package/dist/timecampus/pages/meetings/MeetingDetailPage.js +1 -1
  115. package/dist/timecampus/pages/meetings/MeetingDetailPage.js.map +1 -1
  116. package/dist/timecampus/pages/meetings/MeetingFormModal.d.ts +0 -1
  117. package/dist/timecampus/pages/meetings/MeetingFormModal.js.map +1 -1
  118. package/dist/timecampus/pages/meetings/MeetingsPage.js.map +1 -1
  119. package/dist/timecampus/pages/meetings/ParticipantFormModal.d.ts +0 -1
  120. package/dist/timecampus/pages/meetings/ParticipantFormModal.js.map +1 -1
  121. package/dist/timecampus/pages/pomodoro/PomodoroAnalyticsPage.js.map +1 -1
  122. package/dist/timecampus/pages/pomodoro/PomodoroPage.js.map +1 -1
  123. package/dist/timecampus/pages/pomodoro/PomodoroSettingsPage.js.map +1 -1
  124. package/dist/timecampus/pages/projects/ProjectCreatePage.js.map +1 -1
  125. package/dist/timecampus/pages/projects/ProjectDetailPage.js +1 -1
  126. package/dist/timecampus/pages/projects/ProjectDetailPage.js.map +1 -1
  127. package/dist/timecampus/pages/projects/ProjectsPage.js.map +1 -1
  128. package/dist/timecampus/pages/projects/TasksPage.js +1 -1
  129. package/dist/timecampus/pages/projects/TasksPage.js.map +1 -1
  130. package/dist/timecampus/pages/resource-planning/ResourcePlanningPage.d.ts.map +1 -1
  131. package/dist/timecampus/pages/resource-planning/ResourcePlanningPage.js.map +1 -1
  132. package/dist/timecampus/pages/team/TeamAnalyticsPage.js.map +1 -1
  133. package/dist/timecampus/pages/team/TeamMembersPage.js.map +1 -1
  134. package/dist/timecampus/pages/team/TeamPage.js.map +1 -1
  135. package/dist/timecampus/pages/team-activity/TeamActivityPage.js.map +1 -1
  136. package/dist/timecampus/pages/time/TimeEntriesPage.d.ts.map +1 -1
  137. package/dist/timecampus/pages/time/TimeEntriesPage.js.map +1 -1
  138. package/dist/timecampus/pages/time/TimeTrackingPage.js +1 -1
  139. package/dist/timecampus/pages/time/TimeTrackingPage.js.map +1 -1
  140. package/dist/timecampus/pages/time/TimerPage.js +4 -4
  141. package/dist/timecampus/pages/time/TimerPage.js.map +1 -1
  142. package/dist/timecampus/pages/time-approval/TimeApprovalPage.d.ts.map +1 -1
  143. package/dist/timecampus/pages/time-approval/TimeApprovalPage.js.map +1 -1
  144. package/dist/timecampus/pages/timeline/TimelinePage.js.map +1 -1
  145. package/dist/timecampus/pages/work-patterns/WorkPatternsPage.js +1 -1
  146. package/dist/timecampus/pages/work-patterns/WorkPatternsPage.js.map +1 -1
  147. package/dist/timecampus/providers/TimeCampusProvider.d.ts +0 -1
  148. package/dist/timecampus/providers/TimeCampusProvider.d.ts.map +1 -1
  149. package/dist/timecampus/providers/TimeCampusProvider.js +39 -36
  150. package/dist/timecampus/providers/TimeCampusProvider.js.map +1 -1
  151. package/dist/timecampus/store/timecampusStore.js.map +1 -1
  152. package/dist/timecampus/types/index.d.ts +4 -1
  153. package/dist/timecampus/types/index.d.ts.map +1 -1
  154. package/dist/timecampus/utils/helpers.d.ts +0 -1
  155. package/dist/timecampus/utils/helpers.js.map +1 -1
  156. package/dist/types/assistant.d.ts +0 -1
  157. package/dist/utils/pageContext.js +1 -1
  158. package/dist/utils/pageContext.js.map +1 -1
  159. package/package.json +61 -59
@@ -1 +1 @@
1
- {"version":3,"file":"ClientPortalPage.js","names":[],"sources":["../../../../src/timecampus/pages/client-portal/ClientPortalPage.tsx"],"sourcesContent":["import { useState } from \"react\";\nimport {\n Building2,\n Plus,\n Users,\n PowerOff,\n FolderOpen,\n Link2,\n Check,\n RefreshCw,\n Pencil,\n Trash2,\n Power,\n} from \"lucide-react\";\nimport { GlassCard } from \"@burdenoff/fe-libs/ui\";\nimport { PageLayout } from \"../../components/PageLayout\";\nimport { StatCard } from \"../../components/StatCard\";\nimport { EmptyState } from \"../../components/EmptyState\";\nimport { LoadingSkeleton } from \"../../components/LoadingSkeleton\";\nimport { StatusBadge } from \"../../components/StatusBadge\";\nimport { Modal } from \"../../components/Modal\";\nimport { ToastContainer, useToast } from \"../../components/Toast\";\nimport {\n ChartCard,\n BarsChart,\n DonutChart,\n ProgressMeter,\n} from \"../../components\";\nimport { getInitials, cn, formatDate, formatNumber } from \"../../utils/helpers\";\nimport { gql } from \"@apollo/client\";\nimport { useApolloClient } from \"@apollo/client/react\";\nimport { uploadBlobToFiles } from \"@burdenoff/fe-libs/storage\";\nimport {\n useListClientPortalConfigsQuery,\n useGetClientPortalStatsQuery,\n useRegenerateClientPortalShareTokenMutation,\n useCreateClientPortalConfigMutation,\n useUpdateClientPortalConfigMutation,\n useDeleteClientPortalConfigMutation,\n useToggleClientPortalMutation,\n type ClientPortalFieldsFragment,\n} from \"@/generated/wspace-operations\";\n\nconst PAGE_SIZE = 25;\n\nconst inputClass =\n \"mt-1 w-full rounded-md border border-border-subtle bg-bg-surface px-3 py-2 text-sm text-text-primary placeholder:text-text-secondary\";\n\nconst fileInputClass =\n \"mt-1 block w-full text-sm text-text-secondary file:mr-3 file:rounded-md file:border-0 file:bg-action-primary-bg file:px-3 file:py-1.5 file:text-sm file:font-medium file:text-action-primary-text hover:file:bg-action-primary-bg/90\";\n\n/**\n * Resolve a downloadable URL for a Files object. The URL is presigned and\n * expires after 7 days (604800s) — a known limitation: a stored logoUrl will\n * eventually 403 and the logo must be re-uploaded to refresh it.\n */\nconst GET_FILE_DOWNLOAD_URL = gql`\n query TimeCampus_GetFileDownloadUrl($id: ID!, $expiresIn: Int) {\n getFileDownloadUrl(id: $id, expiresIn: $expiresIn)\n }\n`;\n\n// wspace-files requires a real folder id (File.folderId FK).\nconst ENSURE_FOLDER = gql`\n mutation TimeCampusClient_EnsureFolder($path: String!) {\n ensureFolderByPath(path: $path) {\n id\n }\n }\n`;\n\n/**\n * Build the public share URL for a portal token. The public client view is\n * served by the website at /portal/:token; only active portals resolve.\n */\nfunction shareUrlFor(token: string): string {\n const origin = typeof window !== \"undefined\" ? window.location.origin : \"\";\n return `${origin}/portal/${token}`;\n}\n\ninterface ClientFormState {\n clientName: string;\n contactEmail: string;\n contactPhone: string;\n description: string;\n logoUrl: string;\n}\n\nconst emptyForm: ClientFormState = {\n clientName: \"\",\n contactEmail: \"\",\n contactPhone: \"\",\n description: \"\",\n logoUrl: \"\",\n};\n\nexport function ClientPortalPage() {\n const { toasts, addToast, dismissToast } = useToast();\n const apollo = useApolloClient();\n const [copiedId, setCopiedId] = useState<string | null>(null);\n const [logoUploading, setLogoUploading] = useState(false);\n\n const {\n data: configsData,\n loading: configsLoading,\n error: configsError,\n refetch,\n fetchMore,\n } = useListClientPortalConfigsQuery({\n variables: { pagination: { limit: PAGE_SIZE, offset: 0 } },\n notifyOnNetworkStatusChange: true,\n });\n const { data: statsData, refetch: refetchStats } =\n useGetClientPortalStatsQuery();\n\n const [regenerate, { loading: regenerating }] =\n useRegenerateClientPortalShareTokenMutation();\n const [createClient, { loading: creating }] =\n useCreateClientPortalConfigMutation();\n const [updateClient, { loading: updating }] =\n useUpdateClientPortalConfigMutation();\n const [deleteClient, { loading: deleting }] =\n useDeleteClientPortalConfigMutation();\n const [toggleClient] = useToggleClientPortalMutation();\n\n const clients = configsData?.clientPortalConfigs?.items ?? [];\n const hasMore = configsData?.clientPortalConfigs?.hasMore ?? false;\n const stats = statsData?.clientPortalStats;\n\n const trackedHoursFor = (client: ClientPortalFieldsFragment): number =>\n client.projects.reduce((sum, p) => sum + p.trackedHours, 0);\n\n const inactiveClients =\n (stats?.totalClients ?? 0) - (stats?.activeClients ?? 0);\n\n // Real, already-fetched data only (loaded page of clients + workspace stats).\n const clientHoursData: Array<{ name: string; hours: number }> = clients\n .map((client: ClientPortalFieldsFragment) => ({\n name: client.clientName,\n hours: trackedHoursFor(client),\n }))\n .filter((entry: { name: string; hours: number }) => entry.hours > 0)\n .sort((a: { hours: number }, b: { hours: number }) => b.hours - a.hours)\n .slice(0, 8);\n\n const maxTrackedHours = clients.reduce(\n (max: number, client: ClientPortalFieldsFragment) =>\n Math.max(max, trackedHoursFor(client)),\n 0,\n );\n\n const statusMix = [\n { name: \"Active\", value: stats?.activeClients ?? 0 },\n { name: \"Inactive\", value: Math.max(inactiveClients, 0) },\n ];\n\n const [loadingMore, setLoadingMore] = useState(false);\n\n // Create / edit modal\n const [formOpen, setFormOpen] = useState(false);\n const [editing, setEditing] = useState<ClientPortalFieldsFragment | null>(\n null,\n );\n const [form, setForm] = useState<ClientFormState>(emptyForm);\n\n // Delete confirm\n const [deleteTarget, setDeleteTarget] =\n useState<ClientPortalFieldsFragment | null>(null);\n\n // Per-row busy (toggle/regenerate)\n const [busyId, setBusyId] = useState<string | null>(null);\n\n const refreshAll = async () => {\n await Promise.all([refetch(), refetchStats()]);\n };\n\n const handleCopy = async (client: ClientPortalFieldsFragment) => {\n if (!client.shareToken) return;\n try {\n await navigator.clipboard.writeText(shareUrlFor(client.shareToken));\n setCopiedId(client.id);\n window.setTimeout(() => setCopiedId(null), 2000);\n } catch {\n addToast({ title: \"Clipboard unavailable\", variant: \"error\" });\n }\n };\n\n const handleRegenerate = async (client: ClientPortalFieldsFragment) => {\n setBusyId(client.id);\n try {\n await regenerate({ variables: { id: client.id } });\n addToast({ title: \"Share link regenerated\", variant: \"success\" });\n await refetch();\n } catch {\n addToast({ title: \"Failed to regenerate link\", variant: \"error\" });\n } finally {\n setBusyId(null);\n }\n };\n\n const openCreate = () => {\n setEditing(null);\n setForm(emptyForm);\n setFormOpen(true);\n };\n\n const openEdit = (client: ClientPortalFieldsFragment) => {\n setEditing(client);\n setForm({\n clientName: client.clientName,\n contactEmail: client.contactEmail ?? \"\",\n contactPhone: client.contactPhone ?? \"\",\n description: client.description ?? \"\",\n logoUrl: client.logoUrl ?? \"\",\n });\n setFormOpen(true);\n };\n\n /**\n * Upload a client logo to platform Files and store its presigned URL on the\n * form. Failures surface a toast but never block saving the client — the\n * logo is optional.\n */\n const handleLogoUpload = async (file: File) => {\n setLogoUploading(true);\n try {\n const { data: folderData } = await apollo.mutate({\n mutation: ENSURE_FOLDER,\n variables: { path: \"/TimeCampus/ClientLogos\" },\n });\n const folderId = (\n folderData as { ensureFolderByPath?: { id: string } } | null\n )?.ensureFolderByPath?.id;\n if (!folderId) throw new Error(\"Could not resolve upload folder\");\n const uploaded = await uploadBlobToFiles({\n client: apollo,\n blob: file,\n filename: file.name,\n mimeType: file.type || \"application/octet-stream\",\n folderId,\n });\n const { data } = await apollo.query({\n query: GET_FILE_DOWNLOAD_URL,\n variables: { id: uploaded.id, expiresIn: 604800 },\n fetchPolicy: \"no-cache\",\n });\n const url = (data as { getFileDownloadUrl?: string } | null | undefined)\n ?.getFileDownloadUrl;\n if (!url) throw new Error(\"No download URL returned\");\n setForm((f) => ({ ...f, logoUrl: url }));\n } catch {\n addToast({ title: \"Failed to upload logo\", variant: \"error\" });\n } finally {\n setLogoUploading(false);\n }\n };\n\n const closeForm = () => {\n setFormOpen(false);\n setEditing(null);\n };\n\n const handleSubmit = async () => {\n if (!form.clientName.trim()) return;\n try {\n if (editing) {\n await updateClient({\n variables: {\n id: editing.id,\n input: {\n clientName: form.clientName.trim(),\n contactEmail: form.contactEmail.trim() || undefined,\n contactPhone: form.contactPhone.trim() || undefined,\n description: form.description.trim() || undefined,\n logoUrl: form.logoUrl || undefined,\n },\n },\n });\n addToast({ title: \"Client updated\", variant: \"success\" });\n } else {\n await createClient({\n variables: {\n input: {\n clientName: form.clientName.trim(),\n contactEmail: form.contactEmail.trim() || undefined,\n contactPhone: form.contactPhone.trim() || undefined,\n description: form.description.trim() || undefined,\n logoUrl: form.logoUrl || undefined,\n isActive: true,\n },\n },\n });\n addToast({ title: \"Client added\", variant: \"success\" });\n }\n closeForm();\n await refreshAll();\n } catch {\n addToast({\n title: editing ? \"Failed to update client\" : \"Failed to add client\",\n variant: \"error\",\n });\n }\n };\n\n const handleDelete = async () => {\n if (!deleteTarget) return;\n try {\n await deleteClient({ variables: { id: deleteTarget.id } });\n addToast({ title: \"Client deleted\", variant: \"success\" });\n setDeleteTarget(null);\n await refreshAll();\n } catch {\n addToast({ title: \"Failed to delete client\", variant: \"error\" });\n }\n };\n\n const handleToggle = async (client: ClientPortalFieldsFragment) => {\n setBusyId(client.id);\n try {\n await toggleClient({\n variables: { id: client.id, isActive: !client.isActive },\n });\n addToast({\n title: client.isActive ? \"Client deactivated\" : \"Client activated\",\n variant: \"success\",\n });\n await refreshAll();\n } catch {\n addToast({ title: \"Failed to toggle client\", variant: \"error\" });\n } finally {\n setBusyId(null);\n }\n };\n\n const handleLoadMore = async () => {\n setLoadingMore(true);\n try {\n await fetchMore({\n variables: { pagination: { limit: PAGE_SIZE, offset: clients.length } },\n updateQuery: (prev, { fetchMoreResult }) => {\n if (!fetchMoreResult) return prev;\n return {\n ...prev,\n clientPortalConfigs: {\n ...fetchMoreResult.clientPortalConfigs,\n items: [\n ...prev.clientPortalConfigs.items,\n ...fetchMoreResult.clientPortalConfigs.items,\n ],\n },\n };\n },\n });\n } finally {\n setLoadingMore(false);\n }\n };\n\n if (configsLoading && clients.length === 0) {\n return (\n <PageLayout\n title=\"Client Portal\"\n description=\"Manage clients and their projects\"\n >\n <LoadingSkeleton variant=\"list\" count={4} />\n </PageLayout>\n );\n }\n\n if (configsError) {\n return (\n <PageLayout\n title=\"Client Portal\"\n description=\"Manage clients and their projects\"\n >\n <div className=\"rounded-lg border border-status-error-bg/40 bg-status-error-bg p-4 text-sm text-status-error-text\">\n Failed to load client portal data. Please try again later.\n </div>\n </PageLayout>\n );\n }\n\n const addButton = (\n <button\n onClick={openCreate}\n className=\"inline-flex items-center gap-1.5 rounded-md bg-action-primary-bg px-3 py-2 text-sm font-medium text-action-primary-text hover:bg-action-primary-bg/90\"\n >\n <Plus className=\"h-4 w-4\" />\n Add Client\n </button>\n );\n\n return (\n <PageLayout\n title=\"Client Portal\"\n description=\"Manage clients and their projects\"\n purpose=\"Give clients a transparent window into the work you do for them — the projects, hours and progress tied to each one — so billing conversations are backed by data and trust, not spreadsheets.\"\n actions={addButton}\n >\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4\">\n <StatCard\n featured\n title=\"Active Clients\"\n value={String(stats?.activeClients ?? 0)}\n icon={<Building2 className=\"h-5 w-5\" />}\n />\n <StatCard\n title=\"Total Projects\"\n value={String(stats?.totalProjectsShared ?? 0)}\n icon={<FolderOpen className=\"h-5 w-5\" />}\n />\n <StatCard\n title=\"Total Clients\"\n value={String(stats?.totalClients ?? 0)}\n icon={<Users className=\"h-5 w-5\" />}\n />\n <StatCard\n title=\"Inactive\"\n value={String(Math.max(inactiveClients, 0))}\n icon={<PowerOff className=\"h-5 w-5\" />}\n iconBgColor=\"bg-bg-sunken text-text-secondary\"\n description=\"portals turned off\"\n />\n </div>\n\n {clients.length === 0 ? (\n <EmptyState\n illustration=\"empty-people\"\n title=\"No clients configured yet\"\n description=\"Add your first client to share projects and tracked hours with them.\"\n action={addButton}\n />\n ) : (\n <>\n <div className=\"grid grid-cols-1 gap-4 lg:grid-cols-3\">\n <ChartCard\n title=\"Tracked hours by client\"\n description=\"Top clients by logged hours (loaded page)\"\n className=\"lg:col-span-2\"\n >\n <BarsChart\n data={clientHoursData}\n xKey=\"name\"\n series={[{ key: \"hours\", label: \"Tracked hours\" }]}\n orientation=\"horizontal\"\n height={260}\n categoryWidth={120}\n valueFormat={(value) => `${value.toLocaleString()}h`}\n emptyLabel=\"No tracked hours logged yet.\"\n ariaLabel=\"Tracked hours by client\"\n />\n </ChartCard>\n <ChartCard\n title=\"Portal status\"\n description=\"Shared vs. private clients\"\n >\n <DonutChart\n data={statusMix}\n centerLabel=\"Clients\"\n centerValue={stats?.totalClients ?? 0}\n height={260}\n emptyLabel=\"No clients yet.\"\n ariaLabel=\"Clients by portal status\"\n />\n </ChartCard>\n </div>\n\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 xl:grid-cols-3\">\n {clients.map((client: ClientPortalFieldsFragment) => {\n const totalTrackedHours = trackedHoursFor(client);\n const rowBusy = busyId === client.id;\n const copied = copiedId === client.id;\n return (\n <GlassCard\n key={client.id}\n treatment=\"insight\"\n glow={client.isActive}\n className=\"p-4\"\n >\n <div className=\"flex items-start justify-between gap-3\">\n <div className=\"flex min-w-0 items-center gap-3\">\n {client.logoUrl ? (\n <img\n src={client.logoUrl}\n alt={`${client.clientName} logo`}\n className=\"h-11 w-11 shrink-0 rounded-lg border border-border-seam object-cover\"\n />\n ) : (\n <div className=\"flex h-11 w-11 shrink-0 items-center justify-center rounded-lg bg-action-primary-bg/10 text-sm font-semibold text-primary\">\n {getInitials(client.clientName)}\n </div>\n )}\n <div className=\"min-w-0\">\n <p className=\"truncate text-sm font-semibold text-text-primary\">\n {client.clientName}\n </p>\n <p className=\"truncate text-xs text-text-secondary\">\n Since {formatDate(client.createdAt)}\n </p>\n </div>\n </div>\n <StatusBadge\n label={client.isActive ? \"active\" : \"inactive\"}\n status={client.isActive ? \"active\" : \"inactive\"}\n />\n </div>\n\n <div className=\"mt-4 grid grid-cols-2 gap-3\">\n <div className=\"rounded-lg border border-border-seam bg-bg-sunken/40 px-3 py-2\">\n <p className=\"text-[11px] font-medium uppercase tracking-wide text-text-secondary\">\n Projects\n </p>\n <p className=\"mt-0.5 text-lg font-semibold tabular-nums text-text-primary\">\n {client.projects.length}\n </p>\n </div>\n <div className=\"rounded-lg border border-border-seam bg-bg-sunken/40 px-3 py-2\">\n <p className=\"text-[11px] font-medium uppercase tracking-wide text-text-secondary\">\n Tracked hours\n </p>\n <p className=\"mt-0.5 text-lg font-semibold tabular-nums text-text-primary\">\n {formatNumber(totalTrackedHours)}h\n </p>\n </div>\n </div>\n\n {maxTrackedHours > 0 && (\n <div className=\"mt-3\">\n <ProgressMeter\n value={totalTrackedHours}\n max={maxTrackedHours}\n tone=\"success\"\n size=\"sm\"\n showValue={false}\n label=\"Tracked volume vs. top client\"\n />\n </div>\n )}\n\n <div className=\"mt-4 flex items-center justify-between rounded-lg border border-border-seam bg-bg-sunken/40 px-3 py-2\">\n <span className=\"text-xs font-medium text-text-secondary\">\n Public Access\n </span>\n <button\n type=\"button\"\n disabled={rowBusy}\n onClick={() => void handleToggle(client)}\n title={\n client.isActive\n ? \"Deactivate client portal\"\n : \"Activate client portal\"\n }\n className={cn(\n \"inline-flex items-center gap-1.5 rounded-full px-3 py-1 text-xs font-medium transition-colors disabled:opacity-50\",\n client.isActive\n ? \"bg-status-success-bg text-status-success-text hover:bg-status-success-bg/80\"\n : \"border border-border-seam bg-bg-surface text-text-secondary hover:bg-bg-sunken/60\",\n )}\n >\n <Power className=\"h-3.5 w-3.5\" />\n {client.isActive ? \"Active\" : \"Inactive\"}\n </button>\n </div>\n\n <div className=\"mt-4 flex flex-wrap items-center gap-2 border-t border-border-seam pt-3\">\n {client.isActive && client.shareToken && (\n <>\n <button\n type=\"button\"\n onClick={() => void handleCopy(client)}\n title=\"Copy the client's private share link\"\n className=\"inline-flex items-center gap-1.5 rounded-md border border-border-seam bg-bg-surface px-2.5 py-1.5 text-xs font-medium text-text-secondary hover:bg-bg-sunken/60\"\n >\n {copied ? (\n <>\n <Check className=\"h-3.5 w-3.5\" /> Copied\n </>\n ) : (\n <>\n <Link2 className=\"h-3.5 w-3.5\" /> Share link\n </>\n )}\n </button>\n <button\n type=\"button\"\n disabled={rowBusy && regenerating}\n onClick={() => void handleRegenerate(client)}\n title=\"Regenerate the link (invalidates the old one)\"\n className=\"inline-flex items-center gap-1.5 rounded-md border border-border-seam bg-bg-surface px-2.5 py-1.5 text-xs font-medium text-text-secondary hover:bg-bg-sunken/60 disabled:opacity-50\"\n >\n <RefreshCw\n className={cn(\n \"h-3.5 w-3.5\",\n rowBusy && regenerating && \"animate-spin\",\n )}\n />\n Regenerate\n </button>\n </>\n )}\n <div className=\"ml-auto flex items-center gap-1\">\n <button\n type=\"button\"\n onClick={() => openEdit(client)}\n title=\"Edit client\"\n className=\"inline-flex items-center rounded-md border border-border-seam bg-bg-surface p-1.5 text-text-secondary hover:bg-bg-sunken/60\"\n >\n <Pencil className=\"h-3.5 w-3.5\" />\n </button>\n <button\n type=\"button\"\n onClick={() => setDeleteTarget(client)}\n title=\"Delete client\"\n className=\"inline-flex items-center rounded-md border border-border-seam bg-bg-surface p-1.5 text-status-error-text hover:bg-status-error-bg\"\n >\n <Trash2 className=\"h-3.5 w-3.5\" />\n </button>\n </div>\n </div>\n </GlassCard>\n );\n })}\n </div>\n\n {hasMore && (\n <div className=\"flex justify-center\">\n <button\n onClick={handleLoadMore}\n disabled={loadingMore}\n className=\"rounded-md border border-border-subtle px-4 py-2 text-sm font-medium text-text-primary hover:bg-bg-sunken/50 disabled:opacity-50\"\n >\n {loadingMore ? \"Loading...\" : \"Load more\"}\n </button>\n </div>\n )}\n </>\n )}\n\n <Modal\n open={formOpen}\n onClose={closeForm}\n title={editing ? \"Edit Client\" : \"Add Client\"}\n description={\n editing\n ? \"Update this client's portal details.\"\n : \"Set up a client so you can share projects and tracked hours with them.\"\n }\n footer={\n <>\n <button\n onClick={closeForm}\n className=\"rounded-md border border-border-subtle px-4 py-2 text-sm font-medium text-text-primary hover:bg-bg-sunken/50\"\n >\n Cancel\n </button>\n <button\n onClick={handleSubmit}\n disabled={creating || updating || !form.clientName.trim()}\n className=\"rounded-md bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text hover:bg-action-primary-bg/90 disabled:opacity-50\"\n >\n {creating || updating\n ? \"Saving...\"\n : editing\n ? \"Save Changes\"\n : \"Add Client\"}\n </button>\n </>\n }\n >\n <div className=\"space-y-4\">\n <div>\n <label className=\"block text-sm font-medium text-text-primary\">\n Client Name\n </label>\n <input\n type=\"text\"\n placeholder=\"e.g. Acme Corp\"\n value={form.clientName}\n onChange={(e) => setForm({ ...form, clientName: e.target.value })}\n className={inputClass}\n />\n </div>\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2\">\n <div>\n <label className=\"block text-sm font-medium text-text-primary\">\n Contact Email\n </label>\n <input\n type=\"email\"\n placeholder=\"contact@client.com\"\n value={form.contactEmail}\n onChange={(e) =>\n setForm({ ...form, contactEmail: e.target.value })\n }\n className={inputClass}\n />\n </div>\n <div>\n <label className=\"block text-sm font-medium text-text-primary\">\n Contact Phone\n </label>\n <input\n type=\"tel\"\n placeholder=\"Optional\"\n value={form.contactPhone}\n onChange={(e) =>\n setForm({ ...form, contactPhone: e.target.value })\n }\n className={inputClass}\n />\n </div>\n </div>\n <div>\n <label className=\"block text-sm font-medium text-text-primary\">\n Description\n </label>\n <textarea\n rows={3}\n placeholder=\"Notes about this client (optional)\"\n value={form.description}\n onChange={(e) =>\n setForm({ ...form, description: e.target.value })\n }\n className={inputClass}\n />\n </div>\n <div>\n <label className=\"block text-sm font-medium text-text-primary\">\n Logo\n </label>\n <input\n type=\"file\"\n accept=\"image/*\"\n disabled={logoUploading}\n onChange={(e) => {\n const file = e.target.files?.[0];\n if (file) void handleLogoUpload(file);\n // Reset so re-selecting the same file re-triggers onChange.\n e.target.value = \"\";\n }}\n className={fileInputClass}\n />\n {logoUploading && (\n <p className=\"mt-1 text-xs text-text-secondary\">Uploading…</p>\n )}\n {form.logoUrl && (\n <img\n src={form.logoUrl}\n alt=\"Client logo preview\"\n className=\"mt-2 h-16 w-16 rounded-md border border-border-seam object-cover\"\n />\n )}\n </div>\n </div>\n </Modal>\n\n <Modal\n open={deleteTarget !== null}\n onClose={() => setDeleteTarget(null)}\n title=\"Delete client?\"\n description={\n deleteTarget\n ? `\"${deleteTarget.clientName}\" and its share link will be removed.`\n : undefined\n }\n size=\"sm\"\n footer={\n <>\n <button\n onClick={() => setDeleteTarget(null)}\n className=\"rounded-md border border-border-subtle px-4 py-2 text-sm font-medium text-text-primary hover:bg-bg-sunken/50\"\n >\n Cancel\n </button>\n <button\n onClick={handleDelete}\n disabled={deleting}\n className=\"rounded-md bg-status-error-bg px-4 py-2 text-sm font-medium text-status-error-text hover:bg-status-error-bg/80 disabled:opacity-50\"\n >\n {deleting ? \"Deleting...\" : \"Delete\"}\n </button>\n </>\n }\n >\n <p className=\"text-sm text-text-secondary\">\n The client will lose access to their shared portal immediately. This\n cannot be undone.\n </p>\n </Modal>\n\n <ToastContainer toasts={toasts} onDismiss={dismissToast} />\n </PageLayout>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"ClientPortalPage.js","names":[],"sources":["../../../../src/timecampus/pages/client-portal/ClientPortalPage.tsx"],"sourcesContent":["import { useState } from \"react\";\nimport {\n Building2,\n Plus,\n Users,\n PowerOff,\n FolderOpen,\n Link2,\n Check,\n RefreshCw,\n Pencil,\n Trash2,\n Power,\n} from \"lucide-react\";\nimport { GlassCard } from \"@burdenoff/fe-libs/ui\";\nimport { PageLayout } from \"../../components/PageLayout\";\nimport { StatCard } from \"../../components/StatCard\";\nimport { EmptyState } from \"../../components/EmptyState\";\nimport { LoadingSkeleton } from \"../../components/LoadingSkeleton\";\nimport { StatusBadge } from \"../../components/StatusBadge\";\nimport { Modal } from \"../../components/Modal\";\nimport { ToastContainer, useToast } from \"../../components/Toast\";\nimport {\n ChartCard,\n BarsChart,\n DonutChart,\n ProgressMeter,\n} from \"../../components\";\nimport { getInitials, cn, formatDate, formatNumber } from \"../../utils/helpers\";\nimport { gql } from \"@apollo/client\";\nimport { useApolloClient } from \"@apollo/client/react\";\nimport { uploadBlobToFiles } from \"@burdenoff/fe-libs/storage\";\nimport {\n useListClientPortalConfigsQuery,\n useGetClientPortalStatsQuery,\n useRegenerateClientPortalShareTokenMutation,\n useCreateClientPortalConfigMutation,\n useUpdateClientPortalConfigMutation,\n useDeleteClientPortalConfigMutation,\n useToggleClientPortalMutation,\n type ClientPortalFieldsFragment,\n type ListClientPortalConfigsQuery,\n} from \"@/generated/wspace-operations\";\n\nconst PAGE_SIZE = 25;\n\nconst inputClass =\n \"mt-1 w-full rounded-md border border-border-subtle bg-bg-surface px-3 py-2 text-sm text-text-primary placeholder:text-text-secondary\";\n\nconst fileInputClass =\n \"mt-1 block w-full text-sm text-text-secondary file:mr-3 file:rounded-md file:border-0 file:bg-action-primary-bg file:px-3 file:py-1.5 file:text-sm file:font-medium file:text-action-primary-text hover:file:bg-action-primary-bg/90\";\n\n/**\n * Resolve a downloadable URL for a Files object. The URL is presigned and\n * expires after 7 days (604800s) — a known limitation: a stored logoUrl will\n * eventually 403 and the logo must be re-uploaded to refresh it.\n */\nconst GET_FILE_DOWNLOAD_URL = gql`\n query TimeCampus_GetFileDownloadUrl($id: ID!, $expiresIn: Int) {\n getFileDownloadUrl(id: $id, expiresIn: $expiresIn)\n }\n`;\n\n// wspace-files requires a real folder id (File.folderId FK).\nconst ENSURE_FOLDER = gql`\n mutation TimeCampusClient_EnsureFolder($path: String!) {\n ensureFolderByPath(path: $path) {\n id\n }\n }\n`;\n\n/**\n * Build the public share URL for a portal token. The public client view is\n * served by the website at /portal/:token; only active portals resolve.\n */\nfunction shareUrlFor(token: string): string {\n const origin = typeof window !== \"undefined\" ? window.location.origin : \"\";\n return `${origin}/portal/${token}`;\n}\n\ninterface ClientFormState {\n clientName: string;\n contactEmail: string;\n contactPhone: string;\n description: string;\n logoUrl: string;\n}\n\nconst emptyForm: ClientFormState = {\n clientName: \"\",\n contactEmail: \"\",\n contactPhone: \"\",\n description: \"\",\n logoUrl: \"\",\n};\n\nexport function ClientPortalPage() {\n const { toasts, addToast, dismissToast } = useToast();\n const apollo = useApolloClient();\n const [copiedId, setCopiedId] = useState<string | null>(null);\n const [logoUploading, setLogoUploading] = useState(false);\n\n const {\n data: configsData,\n loading: configsLoading,\n error: configsError,\n refetch,\n fetchMore,\n } = useListClientPortalConfigsQuery({\n variables: { pagination: { limit: PAGE_SIZE, offset: 0 } },\n notifyOnNetworkStatusChange: true,\n });\n const { data: statsData, refetch: refetchStats } =\n useGetClientPortalStatsQuery();\n\n const [regenerate, { loading: regenerating }] =\n useRegenerateClientPortalShareTokenMutation();\n const [createClient, { loading: creating }] =\n useCreateClientPortalConfigMutation();\n const [updateClient, { loading: updating }] =\n useUpdateClientPortalConfigMutation();\n const [deleteClient, { loading: deleting }] =\n useDeleteClientPortalConfigMutation();\n const [toggleClient] = useToggleClientPortalMutation();\n\n const clients = configsData?.clientPortalConfigs?.items ?? [];\n const hasMore = configsData?.clientPortalConfigs?.hasMore ?? false;\n const stats = statsData?.clientPortalStats;\n\n const trackedHoursFor = (client: ClientPortalFieldsFragment): number =>\n client.projects.reduce((sum, p) => sum + p.trackedHours, 0);\n\n const inactiveClients =\n (stats?.totalClients ?? 0) - (stats?.activeClients ?? 0);\n\n // Real, already-fetched data only (loaded page of clients + workspace stats).\n const clientHoursData: Array<{ name: string; hours: number }> = clients\n .map((client: ClientPortalFieldsFragment) => ({\n name: client.clientName,\n hours: trackedHoursFor(client),\n }))\n .filter((entry: { name: string; hours: number }) => entry.hours > 0)\n .sort((a: { hours: number }, b: { hours: number }) => b.hours - a.hours)\n .slice(0, 8);\n\n const maxTrackedHours = clients.reduce(\n (max: number, client: ClientPortalFieldsFragment) =>\n Math.max(max, trackedHoursFor(client)),\n 0,\n );\n\n const statusMix = [\n { name: \"Active\", value: stats?.activeClients ?? 0 },\n { name: \"Inactive\", value: Math.max(inactiveClients, 0) },\n ];\n\n const [loadingMore, setLoadingMore] = useState(false);\n\n // Create / edit modal\n const [formOpen, setFormOpen] = useState(false);\n const [editing, setEditing] = useState<ClientPortalFieldsFragment | null>(\n null,\n );\n const [form, setForm] = useState<ClientFormState>(emptyForm);\n\n // Delete confirm\n const [deleteTarget, setDeleteTarget] =\n useState<ClientPortalFieldsFragment | null>(null);\n\n // Per-row busy (toggle/regenerate)\n const [busyId, setBusyId] = useState<string | null>(null);\n\n const refreshAll = async () => {\n await Promise.all([refetch(), refetchStats()]);\n };\n\n const handleCopy = async (client: ClientPortalFieldsFragment) => {\n if (!client.shareToken) return;\n try {\n await navigator.clipboard.writeText(shareUrlFor(client.shareToken));\n setCopiedId(client.id);\n window.setTimeout(() => setCopiedId(null), 2000);\n } catch {\n addToast({ title: \"Clipboard unavailable\", variant: \"error\" });\n }\n };\n\n const handleRegenerate = async (client: ClientPortalFieldsFragment) => {\n setBusyId(client.id);\n try {\n await regenerate({ variables: { id: client.id } });\n addToast({ title: \"Share link regenerated\", variant: \"success\" });\n await refetch();\n } catch {\n addToast({ title: \"Failed to regenerate link\", variant: \"error\" });\n } finally {\n setBusyId(null);\n }\n };\n\n const openCreate = () => {\n setEditing(null);\n setForm(emptyForm);\n setFormOpen(true);\n };\n\n const openEdit = (client: ClientPortalFieldsFragment) => {\n setEditing(client);\n setForm({\n clientName: client.clientName,\n contactEmail: client.contactEmail ?? \"\",\n contactPhone: client.contactPhone ?? \"\",\n description: client.description ?? \"\",\n logoUrl: client.logoUrl ?? \"\",\n });\n setFormOpen(true);\n };\n\n /**\n * Upload a client logo to platform Files and store its presigned URL on the\n * form. Failures surface a toast but never block saving the client — the\n * logo is optional.\n */\n const handleLogoUpload = async (file: File) => {\n setLogoUploading(true);\n try {\n const { data: folderData } = await apollo.mutate({\n mutation: ENSURE_FOLDER,\n variables: { path: \"/TimeCampus/ClientLogos\" },\n });\n const folderId = (\n folderData as { ensureFolderByPath?: { id: string } } | null\n )?.ensureFolderByPath?.id;\n if (!folderId) throw new Error(\"Could not resolve upload folder\");\n const uploaded = await uploadBlobToFiles({\n client: apollo,\n blob: file,\n filename: file.name,\n mimeType: file.type || \"application/octet-stream\",\n folderId,\n });\n const { data } = await apollo.query({\n query: GET_FILE_DOWNLOAD_URL,\n variables: { id: uploaded.id, expiresIn: 604800 },\n fetchPolicy: \"no-cache\",\n });\n const url = (data as { getFileDownloadUrl?: string } | null | undefined)\n ?.getFileDownloadUrl;\n if (!url) throw new Error(\"No download URL returned\");\n setForm((f) => ({ ...f, logoUrl: url }));\n } catch {\n addToast({ title: \"Failed to upload logo\", variant: \"error\" });\n } finally {\n setLogoUploading(false);\n }\n };\n\n const closeForm = () => {\n setFormOpen(false);\n setEditing(null);\n };\n\n const handleSubmit = async () => {\n if (!form.clientName.trim()) return;\n try {\n if (editing) {\n await updateClient({\n variables: {\n id: editing.id,\n input: {\n clientName: form.clientName.trim(),\n contactEmail: form.contactEmail.trim() || undefined,\n contactPhone: form.contactPhone.trim() || undefined,\n description: form.description.trim() || undefined,\n logoUrl: form.logoUrl || undefined,\n },\n },\n });\n addToast({ title: \"Client updated\", variant: \"success\" });\n } else {\n await createClient({\n variables: {\n input: {\n clientName: form.clientName.trim(),\n contactEmail: form.contactEmail.trim() || undefined,\n contactPhone: form.contactPhone.trim() || undefined,\n description: form.description.trim() || undefined,\n logoUrl: form.logoUrl || undefined,\n isActive: true,\n },\n },\n });\n addToast({ title: \"Client added\", variant: \"success\" });\n }\n closeForm();\n await refreshAll();\n } catch {\n addToast({\n title: editing ? \"Failed to update client\" : \"Failed to add client\",\n variant: \"error\",\n });\n }\n };\n\n const handleDelete = async () => {\n if (!deleteTarget) return;\n try {\n await deleteClient({ variables: { id: deleteTarget.id } });\n addToast({ title: \"Client deleted\", variant: \"success\" });\n setDeleteTarget(null);\n await refreshAll();\n } catch {\n addToast({ title: \"Failed to delete client\", variant: \"error\" });\n }\n };\n\n const handleToggle = async (client: ClientPortalFieldsFragment) => {\n setBusyId(client.id);\n try {\n await toggleClient({\n variables: { id: client.id, isActive: !client.isActive },\n });\n addToast({\n title: client.isActive ? \"Client deactivated\" : \"Client activated\",\n variant: \"success\",\n });\n await refreshAll();\n } catch {\n addToast({ title: \"Failed to toggle client\", variant: \"error\" });\n } finally {\n setBusyId(null);\n }\n };\n\n const handleLoadMore = async () => {\n setLoadingMore(true);\n try {\n await fetchMore({\n variables: { pagination: { limit: PAGE_SIZE, offset: clients.length } },\n updateQuery: (\n prev: ListClientPortalConfigsQuery,\n {\n fetchMoreResult,\n }: { fetchMoreResult?: ListClientPortalConfigsQuery },\n ) => {\n if (!fetchMoreResult) return prev;\n return {\n ...prev,\n clientPortalConfigs: {\n ...fetchMoreResult.clientPortalConfigs,\n items: [\n ...prev.clientPortalConfigs.items,\n ...fetchMoreResult.clientPortalConfigs.items,\n ],\n },\n };\n },\n });\n } finally {\n setLoadingMore(false);\n }\n };\n\n if (configsLoading && clients.length === 0) {\n return (\n <PageLayout\n title=\"Client Portal\"\n description=\"Manage clients and their projects\"\n >\n <LoadingSkeleton variant=\"list\" count={4} />\n </PageLayout>\n );\n }\n\n if (configsError) {\n return (\n <PageLayout\n title=\"Client Portal\"\n description=\"Manage clients and their projects\"\n >\n <div className=\"rounded-lg border border-status-error-bg/40 bg-status-error-bg p-4 text-sm text-status-error-text\">\n Failed to load client portal data. Please try again later.\n </div>\n </PageLayout>\n );\n }\n\n const addButton = (\n <button\n onClick={openCreate}\n className=\"inline-flex items-center gap-1.5 rounded-md bg-action-primary-bg px-3 py-2 text-sm font-medium text-action-primary-text hover:bg-action-primary-bg/90\"\n >\n <Plus className=\"h-4 w-4\" />\n Add Client\n </button>\n );\n\n return (\n <PageLayout\n title=\"Client Portal\"\n description=\"Manage clients and their projects\"\n purpose=\"Give clients a transparent window into the work you do for them — the projects, hours and progress tied to each one — so billing conversations are backed by data and trust, not spreadsheets.\"\n actions={addButton}\n >\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4\">\n <StatCard\n featured\n title=\"Active Clients\"\n value={String(stats?.activeClients ?? 0)}\n icon={<Building2 className=\"h-5 w-5\" />}\n />\n <StatCard\n title=\"Total Projects\"\n value={String(stats?.totalProjectsShared ?? 0)}\n icon={<FolderOpen className=\"h-5 w-5\" />}\n />\n <StatCard\n title=\"Total Clients\"\n value={String(stats?.totalClients ?? 0)}\n icon={<Users className=\"h-5 w-5\" />}\n />\n <StatCard\n title=\"Inactive\"\n value={String(Math.max(inactiveClients, 0))}\n icon={<PowerOff className=\"h-5 w-5\" />}\n iconBgColor=\"bg-bg-sunken text-text-secondary\"\n description=\"portals turned off\"\n />\n </div>\n\n {clients.length === 0 ? (\n <EmptyState\n illustration=\"empty-people\"\n title=\"No clients configured yet\"\n description=\"Add your first client to share projects and tracked hours with them.\"\n action={addButton}\n />\n ) : (\n <>\n <div className=\"grid grid-cols-1 gap-4 lg:grid-cols-3\">\n <ChartCard\n title=\"Tracked hours by client\"\n description=\"Top clients by logged hours (loaded page)\"\n className=\"lg:col-span-2\"\n >\n <BarsChart\n data={clientHoursData}\n xKey=\"name\"\n series={[{ key: \"hours\", label: \"Tracked hours\" }]}\n orientation=\"horizontal\"\n height={260}\n categoryWidth={120}\n valueFormat={(value) => `${value.toLocaleString()}h`}\n emptyLabel=\"No tracked hours logged yet.\"\n ariaLabel=\"Tracked hours by client\"\n />\n </ChartCard>\n <ChartCard\n title=\"Portal status\"\n description=\"Shared vs. private clients\"\n >\n <DonutChart\n data={statusMix}\n centerLabel=\"Clients\"\n centerValue={stats?.totalClients ?? 0}\n height={260}\n emptyLabel=\"No clients yet.\"\n ariaLabel=\"Clients by portal status\"\n />\n </ChartCard>\n </div>\n\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 xl:grid-cols-3\">\n {clients.map((client: ClientPortalFieldsFragment) => {\n const totalTrackedHours = trackedHoursFor(client);\n const rowBusy = busyId === client.id;\n const copied = copiedId === client.id;\n return (\n <GlassCard\n key={client.id}\n treatment=\"insight\"\n glow={client.isActive}\n className=\"p-4\"\n >\n <div className=\"flex items-start justify-between gap-3\">\n <div className=\"flex min-w-0 items-center gap-3\">\n {client.logoUrl ? (\n <img\n src={client.logoUrl}\n alt={`${client.clientName} logo`}\n className=\"h-11 w-11 shrink-0 rounded-lg border border-border-seam object-cover\"\n />\n ) : (\n <div className=\"flex h-11 w-11 shrink-0 items-center justify-center rounded-lg bg-action-primary-bg/10 text-sm font-semibold text-primary\">\n {getInitials(client.clientName)}\n </div>\n )}\n <div className=\"min-w-0\">\n <p className=\"truncate text-sm font-semibold text-text-primary\">\n {client.clientName}\n </p>\n <p className=\"truncate text-xs text-text-secondary\">\n Since {formatDate(client.createdAt)}\n </p>\n </div>\n </div>\n <StatusBadge\n label={client.isActive ? \"active\" : \"inactive\"}\n status={client.isActive ? \"active\" : \"inactive\"}\n />\n </div>\n\n <div className=\"mt-4 grid grid-cols-2 gap-3\">\n <div className=\"rounded-lg border border-border-seam bg-bg-sunken/40 px-3 py-2\">\n <p className=\"text-[11px] font-medium uppercase tracking-wide text-text-secondary\">\n Projects\n </p>\n <p className=\"mt-0.5 text-lg font-semibold tabular-nums text-text-primary\">\n {client.projects.length}\n </p>\n </div>\n <div className=\"rounded-lg border border-border-seam bg-bg-sunken/40 px-3 py-2\">\n <p className=\"text-[11px] font-medium uppercase tracking-wide text-text-secondary\">\n Tracked hours\n </p>\n <p className=\"mt-0.5 text-lg font-semibold tabular-nums text-text-primary\">\n {formatNumber(totalTrackedHours)}h\n </p>\n </div>\n </div>\n\n {maxTrackedHours > 0 && (\n <div className=\"mt-3\">\n <ProgressMeter\n value={totalTrackedHours}\n max={maxTrackedHours}\n tone=\"success\"\n size=\"sm\"\n showValue={false}\n label=\"Tracked volume vs. top client\"\n />\n </div>\n )}\n\n <div className=\"mt-4 flex items-center justify-between rounded-lg border border-border-seam bg-bg-sunken/40 px-3 py-2\">\n <span className=\"text-xs font-medium text-text-secondary\">\n Public Access\n </span>\n <button\n type=\"button\"\n disabled={rowBusy}\n onClick={() => void handleToggle(client)}\n title={\n client.isActive\n ? \"Deactivate client portal\"\n : \"Activate client portal\"\n }\n className={cn(\n \"inline-flex items-center gap-1.5 rounded-full px-3 py-1 text-xs font-medium transition-colors disabled:opacity-50\",\n client.isActive\n ? \"bg-status-success-bg text-status-success-text hover:bg-status-success-bg/80\"\n : \"border border-border-seam bg-bg-surface text-text-secondary hover:bg-bg-sunken/60\",\n )}\n >\n <Power className=\"h-3.5 w-3.5\" />\n {client.isActive ? \"Active\" : \"Inactive\"}\n </button>\n </div>\n\n <div className=\"mt-4 flex flex-wrap items-center gap-2 border-t border-border-seam pt-3\">\n {client.isActive && client.shareToken && (\n <>\n <button\n type=\"button\"\n onClick={() => void handleCopy(client)}\n title=\"Copy the client's private share link\"\n className=\"inline-flex items-center gap-1.5 rounded-md border border-border-seam bg-bg-surface px-2.5 py-1.5 text-xs font-medium text-text-secondary hover:bg-bg-sunken/60\"\n >\n {copied ? (\n <>\n <Check className=\"h-3.5 w-3.5\" /> Copied\n </>\n ) : (\n <>\n <Link2 className=\"h-3.5 w-3.5\" /> Share link\n </>\n )}\n </button>\n <button\n type=\"button\"\n disabled={rowBusy && regenerating}\n onClick={() => void handleRegenerate(client)}\n title=\"Regenerate the link (invalidates the old one)\"\n className=\"inline-flex items-center gap-1.5 rounded-md border border-border-seam bg-bg-surface px-2.5 py-1.5 text-xs font-medium text-text-secondary hover:bg-bg-sunken/60 disabled:opacity-50\"\n >\n <RefreshCw\n className={cn(\n \"h-3.5 w-3.5\",\n rowBusy && regenerating && \"animate-spin\",\n )}\n />\n Regenerate\n </button>\n </>\n )}\n <div className=\"ml-auto flex items-center gap-1\">\n <button\n type=\"button\"\n onClick={() => openEdit(client)}\n title=\"Edit client\"\n className=\"inline-flex items-center rounded-md border border-border-seam bg-bg-surface p-1.5 text-text-secondary hover:bg-bg-sunken/60\"\n >\n <Pencil className=\"h-3.5 w-3.5\" />\n </button>\n <button\n type=\"button\"\n onClick={() => setDeleteTarget(client)}\n title=\"Delete client\"\n className=\"inline-flex items-center rounded-md border border-border-seam bg-bg-surface p-1.5 text-status-error-text hover:bg-status-error-bg\"\n >\n <Trash2 className=\"h-3.5 w-3.5\" />\n </button>\n </div>\n </div>\n </GlassCard>\n );\n })}\n </div>\n\n {hasMore && (\n <div className=\"flex justify-center\">\n <button\n onClick={handleLoadMore}\n disabled={loadingMore}\n className=\"rounded-md border border-border-subtle px-4 py-2 text-sm font-medium text-text-primary hover:bg-bg-sunken/50 disabled:opacity-50\"\n >\n {loadingMore ? \"Loading...\" : \"Load more\"}\n </button>\n </div>\n )}\n </>\n )}\n\n <Modal\n open={formOpen}\n onClose={closeForm}\n title={editing ? \"Edit Client\" : \"Add Client\"}\n description={\n editing\n ? \"Update this client's portal details.\"\n : \"Set up a client so you can share projects and tracked hours with them.\"\n }\n footer={\n <>\n <button\n onClick={closeForm}\n className=\"rounded-md border border-border-subtle px-4 py-2 text-sm font-medium text-text-primary hover:bg-bg-sunken/50\"\n >\n Cancel\n </button>\n <button\n onClick={handleSubmit}\n disabled={creating || updating || !form.clientName.trim()}\n className=\"rounded-md bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text hover:bg-action-primary-bg/90 disabled:opacity-50\"\n >\n {creating || updating\n ? \"Saving...\"\n : editing\n ? \"Save Changes\"\n : \"Add Client\"}\n </button>\n </>\n }\n >\n <div className=\"space-y-4\">\n <div>\n <label className=\"block text-sm font-medium text-text-primary\">\n Client Name\n </label>\n <input\n type=\"text\"\n placeholder=\"e.g. Acme Corp\"\n value={form.clientName}\n onChange={(e) => setForm({ ...form, clientName: e.target.value })}\n className={inputClass}\n />\n </div>\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2\">\n <div>\n <label className=\"block text-sm font-medium text-text-primary\">\n Contact Email\n </label>\n <input\n type=\"email\"\n placeholder=\"contact@client.com\"\n value={form.contactEmail}\n onChange={(e) =>\n setForm({ ...form, contactEmail: e.target.value })\n }\n className={inputClass}\n />\n </div>\n <div>\n <label className=\"block text-sm font-medium text-text-primary\">\n Contact Phone\n </label>\n <input\n type=\"tel\"\n placeholder=\"Optional\"\n value={form.contactPhone}\n onChange={(e) =>\n setForm({ ...form, contactPhone: e.target.value })\n }\n className={inputClass}\n />\n </div>\n </div>\n <div>\n <label className=\"block text-sm font-medium text-text-primary\">\n Description\n </label>\n <textarea\n rows={3}\n placeholder=\"Notes about this client (optional)\"\n value={form.description}\n onChange={(e) =>\n setForm({ ...form, description: e.target.value })\n }\n className={inputClass}\n />\n </div>\n <div>\n <label className=\"block text-sm font-medium text-text-primary\">\n Logo\n </label>\n <input\n type=\"file\"\n accept=\"image/*\"\n disabled={logoUploading}\n onChange={(e) => {\n const file = e.target.files?.[0];\n if (file) void handleLogoUpload(file);\n // Reset so re-selecting the same file re-triggers onChange.\n e.target.value = \"\";\n }}\n className={fileInputClass}\n />\n {logoUploading && (\n <p className=\"mt-1 text-xs text-text-secondary\">Uploading…</p>\n )}\n {form.logoUrl && (\n <img\n src={form.logoUrl}\n alt=\"Client logo preview\"\n className=\"mt-2 h-16 w-16 rounded-md border border-border-seam object-cover\"\n />\n )}\n </div>\n </div>\n </Modal>\n\n <Modal\n open={deleteTarget !== null}\n onClose={() => setDeleteTarget(null)}\n title=\"Delete client?\"\n description={\n deleteTarget\n ? `\"${deleteTarget.clientName}\" and its share link will be removed.`\n : undefined\n }\n size=\"sm\"\n footer={\n <>\n <button\n onClick={() => setDeleteTarget(null)}\n className=\"rounded-md border border-border-subtle px-4 py-2 text-sm font-medium text-text-primary hover:bg-bg-sunken/50\"\n >\n Cancel\n </button>\n <button\n onClick={handleDelete}\n disabled={deleting}\n className=\"rounded-md bg-status-error-bg px-4 py-2 text-sm font-medium text-status-error-text hover:bg-status-error-bg/80 disabled:opacity-50\"\n >\n {deleting ? \"Deleting...\" : \"Delete\"}\n </button>\n </>\n }\n >\n <p className=\"text-sm text-text-secondary\">\n The client will lose access to their shared portal immediately. This\n cannot be undone.\n </p>\n </Modal>\n\n <ToastContainer toasts={toasts} onDismiss={dismissToast} />\n </PageLayout>\n );\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"AnalyticsPage.js","names":[],"sources":["../../../../src/timecampus/pages/dashboard/AnalyticsPage.tsx"],"sourcesContent":["import {\n Clock,\n DollarSign,\n Brain,\n TrendingUp,\n Gauge,\n PieChart as PieIcon,\n CalendarRange,\n Lightbulb,\n} from \"lucide-react\";\nimport { NextSteps } from \"@burdenoff/fe-libs/ui\";\nimport { PageLayout } from \"../../components/PageLayout\";\nimport { LoadingSkeleton } from \"../../components/LoadingSkeleton\";\nimport {\n StatCard,\n StatusBadge,\n ChartCard,\n RadialGauge,\n DonutChart,\n BarsChart,\n type DonutSlice,\n} from \"../../components\";\nimport { formatHours } from \"../../utils/helpers\";\nimport {\n useTimeCampus,\n useTimeCampusNavigate,\n} from \"../../providers/TimeCampusProvider\";\nimport {\n WorkPatternInsightType,\n type WorkPatternInsight,\n} from \"@/generated/wspace-types\";\nimport {\n useGetTimeEntryStatsQuery,\n useGetFocusScoreSummaryQuery,\n useGetWorkPatternSummaryQuery,\n} from \"@/generated/wspace-operations\";\n\ntype BadgeVariant = \"success\" | \"warning\" | \"error\" | \"info\" | \"muted\";\n\nconst INSIGHT_META: Record<\n WorkPatternInsightType,\n { variant: BadgeVariant; label: string }\n> = {\n [WorkPatternInsightType.Positive]: { variant: \"success\", label: \"Positive\" },\n [WorkPatternInsightType.Negative]: { variant: \"error\", label: \"Needs work\" },\n [WorkPatternInsightType.Warning]: { variant: \"warning\", label: \"Warning\" },\n [WorkPatternInsightType.Recommendation]: { variant: \"info\", label: \"Tip\" },\n [WorkPatternInsightType.Neutral]: { variant: \"muted\", label: \"Insight\" },\n};\n\nexport function AnalyticsPage() {\n const { workspaceId = \"\" } = useTimeCampus();\n const navigate = useTimeCampusNavigate();\n\n const {\n data: statsData,\n loading: statsLoading,\n error: statsError,\n } = useGetTimeEntryStatsQuery({\n skip: !workspaceId,\n });\n\n const { data: focusData, loading: focusLoading } =\n useGetFocusScoreSummaryQuery({\n skip: !workspaceId,\n });\n\n const { data: patternData, loading: patternLoading } =\n useGetWorkPatternSummaryQuery({\n skip: !workspaceId,\n });\n\n const stats = statsData?.timeEntryStats;\n const focus = focusData?.focusScoreSummary;\n const pattern = patternData?.workPatternSummary;\n\n if (statsLoading || focusLoading || patternLoading) {\n return (\n <PageLayout\n title=\"Analytics\"\n description=\"Insights into your time tracking and productivity\"\n >\n <LoadingSkeleton variant=\"card\" count={4} />\n </PageLayout>\n );\n }\n\n if (statsError) {\n return (\n <PageLayout\n title=\"Analytics\"\n description=\"Insights into your time tracking and productivity\"\n >\n <div className=\"rounded-lg border border-status-error-bg/40 bg-status-error-bg p-4 text-sm text-status-error-text\">\n Failed to load analytics. Please try again later.\n </div>\n </PageLayout>\n );\n }\n\n // Billable vs non-billable split from the real time-entry aggregate.\n const billableHours = stats\n ? Math.max(0, Math.round(stats.billableHours))\n : 0;\n const nonBillableHours = stats\n ? Math.max(0, Math.round(stats.totalHours - stats.billableHours))\n : 0;\n const billablePct =\n stats && stats.totalHours > 0\n ? Math.round((stats.billableHours / stats.totalHours) * 100)\n : 0;\n const distributionSlices: DonutSlice[] = [\n { name: \"Billable\", value: billableHours },\n { name: \"Non-billable\", value: nonBillableHours },\n ];\n\n // Average-day split from the real work-pattern aggregate.\n const dayBreakdown = pattern\n ? [\n { label: \"Focus\", hours: Math.max(0, pattern.averageFocusHours) },\n { label: \"Meetings\", hours: Math.max(0, pattern.averageMeetingHours) },\n ]\n : [];\n\n const trendText = focus?.trend\n ? focus.trend.charAt(0).toUpperCase() + focus.trend.slice(1)\n : undefined;\n\n return (\n <PageLayout\n title=\"Analytics\"\n description=\"Insights into your time tracking and productivity\"\n purpose=\"Turn the raw hours you track into answers — spot whether your focus is improving, where billable time is going, and which habits to change to get more done with less burnout.\"\n >\n <NextSteps\n storageKey=\"timecampus-analytics\"\n className=\"mb-6\"\n steps={[\n {\n id: \"reports\",\n label: \"Build a detailed report\",\n description: \"Slice time by project, client or person.\",\n onClick: () => navigate(\"/reports\"),\n },\n {\n id: \"focus\",\n label: \"Dig into your focus score\",\n description: \"Understand what's helping or hurting deep work.\",\n onClick: () => navigate(\"/focus-score\"),\n },\n {\n id: \"patterns\",\n label: \"Review your work patterns\",\n description: \"See how your week actually breaks down.\",\n onClick: () => navigate(\"/work-patterns\"),\n },\n ]}\n />\n\n {/* KPI row */}\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4\">\n <StatCard\n title=\"Total Hours\"\n value={stats ? formatHours(stats.totalHours) : \"--\"}\n icon={<Clock className=\"h-5 w-5\" />}\n description={stats ? `${stats.totalEntries} entries` : undefined}\n />\n <StatCard\n title=\"Billable Hours\"\n value={stats ? formatHours(stats.billableHours) : \"--\"}\n icon={<DollarSign className=\"h-5 w-5\" />}\n description={stats ? `${billablePct}% of tracked time` : undefined}\n iconBgColor=\"bg-status-success-bg text-status-success-text\"\n />\n <StatCard\n featured\n title=\"Avg Focus Score\"\n value={focus ? `${Math.round(focus.averageScore)}` : \"--\"}\n icon={<Brain className=\"h-5 w-5\" />}\n description={trendText ? `${trendText} trend` : undefined}\n iconBgColor=\"bg-status-info-bg text-status-info-text\"\n />\n <StatCard\n title=\"Avg Productivity\"\n value={pattern ? `${Math.round(pattern.averageProductivity)}%` : \"--\"}\n icon={<TrendingUp className=\"h-5 w-5\" />}\n description={\n stats && stats.activeTimers > 0\n ? `${stats.activeTimers} timer(s) running`\n : undefined\n }\n />\n </div>\n\n {/* Focus gauge + time distribution */}\n <div className=\"mt-6 grid grid-cols-1 gap-6 lg:grid-cols-2\">\n <ChartCard\n title=\"Focus Score\"\n description=\"Your average deep-work score across the tracked window\"\n actions={<Gauge className=\"h-4 w-4 text-text-secondary\" />}\n >\n {focus ? (\n <div className=\"space-y-4\">\n <RadialGauge\n value={focus.averageScore}\n min={0}\n max={100}\n label=\"Avg score\"\n sublabel={trendText ? `${trendText} trend` : undefined}\n thresholds={{ warning: 75, danger: 50 }}\n height={196}\n />\n <div className=\"grid grid-cols-2 gap-3\">\n <div className=\"rounded-lg border border-border-seam bg-bg-sunken/40 p-3\">\n <p className=\"text-xs text-text-secondary\">Lowest</p>\n <p className=\"mt-0.5 text-lg font-semibold tabular-nums text-text-primary\">\n {Math.round(focus.lowestScore)}\n </p>\n </div>\n <div className=\"rounded-lg border border-border-seam bg-bg-sunken/40 p-3\">\n <p className=\"text-xs text-text-secondary\">Highest</p>\n <p className=\"mt-0.5 text-lg font-semibold tabular-nums text-text-primary\">\n {Math.round(focus.highestScore)}\n </p>\n </div>\n </div>\n {(focus.topFactor || focus.improvementArea) && (\n <div className=\"flex flex-wrap items-center gap-2\">\n {focus.topFactor && (\n <StatusBadge\n variant=\"success\"\n label={`Strength: ${focus.topFactor}`}\n />\n )}\n {focus.improvementArea && (\n <StatusBadge\n variant=\"warning\"\n label={`Improve: ${focus.improvementArea}`}\n />\n )}\n </div>\n )}\n </div>\n ) : (\n <p className=\"py-8 text-center text-sm text-text-secondary\">\n No focus score data yet.\n </p>\n )}\n </ChartCard>\n\n <ChartCard\n title=\"Time Distribution\"\n description=\"Billable vs non-billable across everything you tracked\"\n actions={<PieIcon className=\"h-4 w-4 text-text-secondary\" />}\n >\n {stats && stats.totalHours > 0 ? (\n <DonutChart\n data={distributionSlices}\n centerLabel=\"Tracked\"\n centerValue={`${Math.round(stats.totalHours)}h`}\n valueFormat={formatHours}\n height={240}\n ariaLabel=\"Billable versus non-billable hours\"\n />\n ) : (\n <p className=\"py-8 text-center text-sm text-text-secondary\">\n No time entries tracked yet.\n </p>\n )}\n </ChartCard>\n </div>\n\n {/* Work pattern insights */}\n {pattern && (\n <ChartCard\n title=\"Work Patterns\"\n description=\"How your average day splits and what to change\"\n actions={<CalendarRange className=\"h-4 w-4 text-text-secondary\" />}\n className=\"mt-6\"\n >\n <div className=\"grid grid-cols-1 gap-6 lg:grid-cols-2\">\n <div className=\"space-y-4\">\n <div className=\"flex flex-wrap gap-2\">\n {pattern.mostProductiveDay && (\n <StatusBadge\n variant=\"success\"\n label={`Peak: ${pattern.mostProductiveDay}`}\n />\n )}\n {pattern.leastProductiveDay && (\n <StatusBadge\n variant=\"muted\"\n label={`Quietest: ${pattern.leastProductiveDay}`}\n />\n )}\n </div>\n {dayBreakdown.some((d) => d.hours > 0) ? (\n <BarsChart\n data={dayBreakdown}\n xKey=\"label\"\n series={[{ key: \"hours\", label: \"Avg hours / day\" }]}\n orientation=\"horizontal\"\n valueFormat={formatHours}\n categoryWidth={80}\n height={140}\n ariaLabel=\"Average focus versus meeting hours per day\"\n />\n ) : (\n <p className=\"text-sm text-text-secondary\">\n Not enough tracked days to break down your average day yet.\n </p>\n )}\n </div>\n\n <div className=\"space-y-3\">\n {pattern.insights.length === 0 ? (\n <p className=\"text-sm text-text-secondary\">\n No pattern insights surfaced yet — keep tracking and they'll\n appear here.\n </p>\n ) : (\n pattern.insights.map(\n (insight: WorkPatternInsight, index: number) => {\n const meta = INSIGHT_META[insight.type];\n return (\n <div\n key={`${insight.title}-${index}`}\n className=\"rounded-lg border border-border-subtle bg-bg-sunken/40 p-3\"\n >\n <div className=\"flex items-start justify-between gap-3\">\n <p className=\"text-sm font-medium text-text-primary\">\n {insight.title}\n </p>\n <StatusBadge\n variant={meta.variant}\n label={meta.label}\n />\n </div>\n <p className=\"mt-1 text-xs text-text-secondary\">\n {insight.description}\n </p>\n {insight.recommendation && (\n <p className=\"mt-1.5 flex items-start gap-1.5 text-xs text-text-secondary\">\n <Lightbulb className=\"mt-0.5 h-3.5 w-3.5 shrink-0 text-status-info-text\" />\n <span>{insight.recommendation}</span>\n </p>\n )}\n </div>\n );\n },\n )\n )}\n </div>\n </div>\n </ChartCard>\n )}\n </PageLayout>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"AnalyticsPage.js","names":[],"sources":["../../../../src/timecampus/pages/dashboard/AnalyticsPage.tsx"],"sourcesContent":["import {\n Clock,\n DollarSign,\n Brain,\n TrendingUp,\n Gauge,\n PieChart as PieIcon,\n CalendarRange,\n Lightbulb,\n} from \"lucide-react\";\nimport { NextSteps } from \"@burdenoff/fe-libs/ui\";\nimport { PageLayout } from \"../../components/PageLayout\";\nimport { LoadingSkeleton } from \"../../components/LoadingSkeleton\";\nimport {\n StatCard,\n StatusBadge,\n ChartCard,\n RadialGauge,\n DonutChart,\n BarsChart,\n type DonutSlice,\n} from \"../../components\";\nimport { formatHours } from \"../../utils/helpers\";\nimport {\n useTimeCampus,\n useTimeCampusNavigate,\n} from \"../../providers/TimeCampusProvider\";\nimport {\n WorkPatternInsightType,\n type WorkPatternInsight,\n} from \"@/generated/wspace-types\";\nimport {\n useGetTimeEntryStatsQuery,\n useGetFocusScoreSummaryQuery,\n useGetWorkPatternSummaryQuery,\n} from \"@/generated/wspace-operations\";\n\ntype BadgeVariant = \"success\" | \"warning\" | \"error\" | \"info\" | \"muted\";\n\nconst INSIGHT_META: Record<\n WorkPatternInsightType,\n { variant: BadgeVariant; label: string }\n> = {\n [WorkPatternInsightType.Positive]: { variant: \"success\", label: \"Positive\" },\n [WorkPatternInsightType.Negative]: { variant: \"error\", label: \"Needs work\" },\n [WorkPatternInsightType.Warning]: { variant: \"warning\", label: \"Warning\" },\n [WorkPatternInsightType.Recommendation]: { variant: \"info\", label: \"Tip\" },\n [WorkPatternInsightType.Neutral]: { variant: \"muted\", label: \"Insight\" },\n};\n\nexport function AnalyticsPage() {\n const { workspaceId = \"\" } = useTimeCampus();\n const navigate = useTimeCampusNavigate();\n\n const {\n data: statsData,\n loading: statsLoading,\n error: statsError,\n } = useGetTimeEntryStatsQuery({\n skip: !workspaceId,\n });\n\n const { data: focusData, loading: focusLoading } =\n useGetFocusScoreSummaryQuery({\n skip: !workspaceId,\n });\n\n const { data: patternData, loading: patternLoading } =\n useGetWorkPatternSummaryQuery({\n skip: !workspaceId,\n });\n\n const stats = statsData?.timeEntryStats;\n const focus = focusData?.focusScoreSummary;\n const pattern = patternData?.workPatternSummary;\n\n if (statsLoading || focusLoading || patternLoading) {\n return (\n <PageLayout\n title=\"Analytics\"\n description=\"Insights into your time tracking and productivity\"\n >\n <LoadingSkeleton variant=\"card\" count={4} />\n </PageLayout>\n );\n }\n\n if (statsError) {\n return (\n <PageLayout\n title=\"Analytics\"\n description=\"Insights into your time tracking and productivity\"\n >\n <div className=\"rounded-lg border border-status-error-bg/40 bg-status-error-bg p-4 text-sm text-status-error-text\">\n Failed to load analytics. Please try again later.\n </div>\n </PageLayout>\n );\n }\n\n // Billable vs non-billable split from the real time-entry aggregate.\n const billableHours = stats\n ? Math.max(0, Math.round(stats.billableHours))\n : 0;\n const nonBillableHours = stats\n ? Math.max(0, Math.round(stats.totalHours - stats.billableHours))\n : 0;\n const billablePct =\n stats && stats.totalHours > 0\n ? Math.round((stats.billableHours / stats.totalHours) * 100)\n : 0;\n const distributionSlices: DonutSlice[] = [\n { name: \"Billable\", value: billableHours },\n { name: \"Non-billable\", value: nonBillableHours },\n ];\n\n // Average-day split from the real work-pattern aggregate.\n const dayBreakdown = pattern\n ? [\n { label: \"Focus\", hours: Math.max(0, pattern.averageFocusHours) },\n { label: \"Meetings\", hours: Math.max(0, pattern.averageMeetingHours) },\n ]\n : [];\n\n const trendText = focus?.trend\n ? focus.trend.charAt(0).toUpperCase() + focus.trend.slice(1)\n : undefined;\n\n return (\n <PageLayout\n title=\"Analytics\"\n description=\"Insights into your time tracking and productivity\"\n purpose=\"Turn the raw hours you track into answers — spot whether your focus is improving, where billable time is going, and which habits to change to get more done with less burnout.\"\n >\n <NextSteps\n storageKey=\"timecampus-analytics\"\n className=\"mb-6\"\n steps={[\n {\n id: \"reports\",\n label: \"Build a detailed report\",\n description: \"Slice time by project, client or person.\",\n onClick: () => navigate(\"/reports\"),\n },\n {\n id: \"focus\",\n label: \"Dig into your focus score\",\n description: \"Understand what's helping or hurting deep work.\",\n onClick: () => navigate(\"/focus-score\"),\n },\n {\n id: \"patterns\",\n label: \"Review your work patterns\",\n description: \"See how your week actually breaks down.\",\n onClick: () => navigate(\"/work-patterns\"),\n },\n ]}\n />\n\n {/* KPI row */}\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4\">\n <StatCard\n title=\"Total Hours\"\n value={stats ? formatHours(stats.totalHours) : \"--\"}\n icon={<Clock className=\"h-5 w-5\" />}\n description={stats ? `${stats.totalEntries} entries` : undefined}\n />\n <StatCard\n title=\"Billable Hours\"\n value={stats ? formatHours(stats.billableHours) : \"--\"}\n icon={<DollarSign className=\"h-5 w-5\" />}\n description={stats ? `${billablePct}% of tracked time` : undefined}\n iconBgColor=\"bg-status-success-bg text-status-success-text\"\n />\n <StatCard\n featured\n title=\"Avg Focus Score\"\n value={focus ? `${Math.round(focus.averageScore)}` : \"--\"}\n icon={<Brain className=\"h-5 w-5\" />}\n description={trendText ? `${trendText} trend` : undefined}\n iconBgColor=\"bg-status-info-bg text-status-info-text\"\n />\n <StatCard\n title=\"Avg Productivity\"\n value={pattern ? `${Math.round(pattern.averageProductivity)}%` : \"--\"}\n icon={<TrendingUp className=\"h-5 w-5\" />}\n description={\n stats && stats.activeTimers > 0\n ? `${stats.activeTimers} timer(s) running`\n : undefined\n }\n />\n </div>\n\n {/* Focus gauge + time distribution */}\n <div className=\"mt-6 grid grid-cols-1 gap-6 lg:grid-cols-2\">\n <ChartCard\n title=\"Focus Score\"\n description=\"Your average deep-work score across the tracked window\"\n actions={<Gauge className=\"h-4 w-4 text-text-secondary\" />}\n >\n {focus ? (\n <div className=\"space-y-4\">\n <RadialGauge\n value={focus.averageScore}\n min={0}\n max={100}\n label=\"Avg score\"\n sublabel={trendText ? `${trendText} trend` : undefined}\n thresholds={{ warning: 75, danger: 50 }}\n height={196}\n />\n <div className=\"grid grid-cols-2 gap-3\">\n <div className=\"rounded-lg border border-border-seam bg-bg-sunken/40 p-3\">\n <p className=\"text-xs text-text-secondary\">Lowest</p>\n <p className=\"mt-0.5 text-lg font-semibold tabular-nums text-text-primary\">\n {Math.round(focus.lowestScore)}\n </p>\n </div>\n <div className=\"rounded-lg border border-border-seam bg-bg-sunken/40 p-3\">\n <p className=\"text-xs text-text-secondary\">Highest</p>\n <p className=\"mt-0.5 text-lg font-semibold tabular-nums text-text-primary\">\n {Math.round(focus.highestScore)}\n </p>\n </div>\n </div>\n {(focus.topFactor || focus.improvementArea) && (\n <div className=\"flex flex-wrap items-center gap-2\">\n {focus.topFactor && (\n <StatusBadge\n variant=\"success\"\n label={`Strength: ${focus.topFactor}`}\n />\n )}\n {focus.improvementArea && (\n <StatusBadge\n variant=\"warning\"\n label={`Improve: ${focus.improvementArea}`}\n />\n )}\n </div>\n )}\n </div>\n ) : (\n <p className=\"py-8 text-center text-sm text-text-secondary\">\n No focus score data yet.\n </p>\n )}\n </ChartCard>\n\n <ChartCard\n title=\"Time Distribution\"\n description=\"Billable vs non-billable across everything you tracked\"\n actions={<PieIcon className=\"h-4 w-4 text-text-secondary\" />}\n >\n {stats && stats.totalHours > 0 ? (\n <DonutChart\n data={distributionSlices}\n centerLabel=\"Tracked\"\n centerValue={`${Math.round(stats.totalHours)}h`}\n valueFormat={formatHours}\n height={240}\n ariaLabel=\"Billable versus non-billable hours\"\n />\n ) : (\n <p className=\"py-8 text-center text-sm text-text-secondary\">\n No time entries tracked yet.\n </p>\n )}\n </ChartCard>\n </div>\n\n {/* Work pattern insights */}\n {pattern && (\n <ChartCard\n title=\"Work Patterns\"\n description=\"How your average day splits and what to change\"\n actions={<CalendarRange className=\"h-4 w-4 text-text-secondary\" />}\n className=\"mt-6\"\n >\n <div className=\"grid grid-cols-1 gap-6 lg:grid-cols-2\">\n <div className=\"space-y-4\">\n <div className=\"flex flex-wrap gap-2\">\n {pattern.mostProductiveDay && (\n <StatusBadge\n variant=\"success\"\n label={`Peak: ${pattern.mostProductiveDay}`}\n />\n )}\n {pattern.leastProductiveDay && (\n <StatusBadge\n variant=\"muted\"\n label={`Quietest: ${pattern.leastProductiveDay}`}\n />\n )}\n </div>\n {dayBreakdown.some((d) => d.hours > 0) ? (\n <BarsChart\n data={dayBreakdown}\n xKey=\"label\"\n series={[{ key: \"hours\", label: \"Avg hours / day\" }]}\n orientation=\"horizontal\"\n valueFormat={formatHours}\n categoryWidth={80}\n height={140}\n ariaLabel=\"Average focus versus meeting hours per day\"\n />\n ) : (\n <p className=\"text-sm text-text-secondary\">\n Not enough tracked days to break down your average day yet.\n </p>\n )}\n </div>\n\n <div className=\"space-y-3\">\n {pattern.insights.length === 0 ? (\n <p className=\"text-sm text-text-secondary\">\n No pattern insights surfaced yet — keep tracking and they'll\n appear here.\n </p>\n ) : (\n pattern.insights.map(\n (insight: WorkPatternInsight, index: number) => {\n const meta = INSIGHT_META[insight.type];\n return (\n <div\n key={`${insight.title}-${index}`}\n className=\"rounded-lg border border-border-subtle bg-bg-sunken/40 p-3\"\n >\n <div className=\"flex items-start justify-between gap-3\">\n <p className=\"text-sm font-medium text-text-primary\">\n {insight.title}\n </p>\n <StatusBadge\n variant={meta.variant}\n label={meta.label}\n />\n </div>\n <p className=\"mt-1 text-xs text-text-secondary\">\n {insight.description}\n </p>\n {insight.recommendation && (\n <p className=\"mt-1.5 flex items-start gap-1.5 text-xs text-text-secondary\">\n <Lightbulb className=\"mt-0.5 h-3.5 w-3.5 shrink-0 text-status-info-text\" />\n <span>{insight.recommendation}</span>\n </p>\n )}\n </div>\n );\n },\n )\n )}\n </div>\n </div>\n </ChartCard>\n )}\n </PageLayout>\n );\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"ReportsPage.js","names":[],"sources":["../../../../src/timecampus/pages/dashboard/ReportsPage.tsx"],"sourcesContent":["import { useState, type ReactNode } from \"react\";\nimport {\n RefreshCw,\n Download,\n Clock,\n DollarSign,\n CalendarRange,\n Brain,\n} from \"lucide-react\";\nimport { GlassCard } from \"@burdenoff/fe-libs/ui\";\nimport { PageLayout } from \"../../components/PageLayout\";\nimport { EmptyState } from \"../../components/EmptyState\";\nimport { LoadingSkeleton } from \"../../components/LoadingSkeleton\";\nimport {\n StatusBadge,\n ProgressMeter,\n RadialGauge,\n BarsChart,\n} from \"../../components\";\nimport { cn, formatHours } from \"../../utils/helpers\";\n\n/** Trigger a client-side CSV download from an array of [key, value] rows. */\nfunction downloadCsv(filename: string, rows: Array<[string, string | number]>) {\n const escape = (v: string | number) => `\"${String(v).replace(/\"/g, '\"\"')}\"`;\n const csv = [\n \"Metric,Value\",\n ...rows.map(([k, v]) => `${escape(k)},${escape(v)}`),\n ].join(\"\\n\");\n const blob = new Blob([csv], { type: \"text/csv;charset=utf-8;\" });\n const url = URL.createObjectURL(blob);\n const link = document.createElement(\"a\");\n link.href = url;\n link.download = filename;\n document.body.appendChild(link);\n link.click();\n document.body.removeChild(link);\n URL.revokeObjectURL(url);\n}\nimport { useTimeCampus } from \"../../providers/TimeCampusProvider\";\nimport {\n useGetTimeEntryStatsQuery,\n useGetWorkPatternSummaryQuery,\n useGetFocusScoreSummaryQuery,\n} from \"@/generated/wspace-operations\";\n\ntype ReportTypeVariant = \"info\" | \"success\" | \"warning\" | \"default\";\n\ninterface ReportCard {\n id: string;\n name: string;\n typeLabel: string;\n typeVariant: ReportTypeVariant;\n period: string;\n icon: ReactNode;\n accent: string;\n visual: ReactNode;\n}\n\nexport function ReportsPage() {\n const { workspaceId = \"\" } = useTimeCampus();\n const [refreshing, setRefreshing] = useState(false);\n\n const {\n data: statsData,\n loading: statsLoading,\n error: statsError,\n refetch: refetchStats,\n } = useGetTimeEntryStatsQuery({\n skip: !workspaceId,\n });\n\n const {\n data: patternData,\n loading: patternLoading,\n refetch: refetchPattern,\n } = useGetWorkPatternSummaryQuery({\n skip: !workspaceId,\n });\n\n const {\n data: focusData,\n loading: focusLoading,\n refetch: refetchFocus,\n } = useGetFocusScoreSummaryQuery({\n skip: !workspaceId,\n });\n\n const stats = statsData?.timeEntryStats;\n const pattern = patternData?.workPatternSummary;\n const focus = focusData?.focusScoreSummary;\n\n const handleRefresh = async () => {\n setRefreshing(true);\n try {\n await Promise.all([refetchStats(), refetchPattern(), refetchFocus()]);\n } finally {\n setRefreshing(false);\n }\n };\n\n const handleDownload = (reportId: string) => {\n if (\n (reportId === \"time-summary\" || reportId === \"billable-summary\") &&\n stats\n ) {\n downloadCsv(`${reportId}.csv`, [\n [\"Total hours\", Math.round(stats.totalHours)],\n [\"Billable hours\", Math.round(stats.billableHours)],\n [\"Total entries\", stats.totalEntries],\n [\"Active timers\", stats.activeTimers],\n ]);\n } else if (reportId === \"work-pattern\" && pattern) {\n downloadCsv(\"work-pattern-analysis.csv\", [\n [\"Average focus hours\", pattern.averageFocusHours.toFixed(1)],\n [\"Average productivity %\", Math.round(pattern.averageProductivity)],\n ]);\n } else if (reportId === \"focus-score\" && focus) {\n downloadCsv(\"focus-score-report.csv\", [\n [\"Average score\", Math.round(focus.averageScore)],\n [\"Highest score\", Math.round(focus.highestScore)],\n [\"Lowest score\", Math.round(focus.lowestScore)],\n ]);\n }\n };\n\n const isLoading = statsLoading || patternLoading || focusLoading;\n\n if (isLoading) {\n return (\n <PageLayout\n title=\"Reports\"\n description=\"Generate and view time tracking reports\"\n actions={\n <button\n disabled\n className=\"inline-flex items-center gap-1.5 rounded-md bg-action-primary-bg px-3 py-2 text-sm font-medium text-action-primary-text opacity-70\"\n >\n <RefreshCw className=\"h-4 w-4\" />\n Refresh\n </button>\n }\n >\n <LoadingSkeleton variant=\"list\" count={4} />\n </PageLayout>\n );\n }\n\n if (statsError) {\n return (\n <PageLayout\n title=\"Reports\"\n description=\"Generate and view time tracking reports\"\n >\n <div className=\"rounded-lg border border-status-error-bg/40 bg-status-error-bg p-4 text-sm text-status-error-text\">\n Failed to load reports data. Please try again later.\n </div>\n </PageLayout>\n );\n }\n\n // Derived, real figures shared by the report visuals.\n const nonBillableHours = stats\n ? Math.max(0, stats.totalHours - stats.billableHours)\n : 0;\n const billablePct =\n stats && stats.totalHours > 0\n ? Math.round((stats.billableHours / stats.totalHours) * 100)\n : 0;\n\n // Build dynamic report cards from available analytics data. Each carries a\n // small, real visual fed only by the aggregate the page already fetched.\n const reports: ReportCard[] = [\n ...(stats\n ? [\n {\n id: \"time-summary\",\n name: \"Time Tracking Summary\",\n typeLabel: \"Time\",\n typeVariant: \"info\" as const,\n period: `${formatHours(stats.totalHours)} total · ${stats.totalEntries} entries`,\n icon: <Clock className=\"h-5 w-5\" />,\n accent: \"bg-status-info-bg text-status-info-text\",\n visual: (\n <div className=\"space-y-2\">\n <ProgressMeter\n value={stats.billableHours}\n max={stats.totalHours > 0 ? stats.totalHours : 1}\n higherIsWorse={false}\n label=\"Billable share\"\n format={formatHours}\n size=\"md\"\n />\n <div className=\"flex items-center justify-between text-xs text-text-secondary\">\n <span>{stats.totalEntries} entries logged</span>\n <span>\n {stats.activeTimers > 0\n ? `${stats.activeTimers} timer(s) running`\n : \"No active timers\"}\n </span>\n </div>\n </div>\n ),\n },\n {\n id: \"billable-summary\",\n name: \"Billable Hours Report\",\n typeLabel: \"Billing\",\n typeVariant: \"success\" as const,\n period: `${formatHours(stats.billableHours)} billable · ${billablePct}% of tracked`,\n icon: <DollarSign className=\"h-5 w-5\" />,\n accent: \"bg-status-success-bg text-status-success-text\",\n visual: (\n <BarsChart\n data={[\n { label: \"Billable\", hours: stats.billableHours },\n { label: \"Non-billable\", hours: nonBillableHours },\n ]}\n xKey=\"label\"\n series={[{ key: \"hours\", label: \"Hours\" }]}\n orientation=\"horizontal\"\n valueFormat={formatHours}\n categoryWidth={92}\n height={120}\n showGrid={false}\n ariaLabel=\"Billable versus non-billable hours\"\n />\n ),\n },\n ]\n : []),\n ...(pattern\n ? [\n {\n id: \"work-pattern\",\n name: \"Work Pattern Analysis\",\n typeLabel: \"Patterns\",\n typeVariant: \"default\" as const,\n period: `${formatHours(pattern.averageFocusHours)} avg focus · ${Math.round(pattern.averageProductivity)}% productivity`,\n icon: <CalendarRange className=\"h-5 w-5\" />,\n accent: \"bg-action-primary-bg/10 text-primary\",\n visual: (\n <div className=\"space-y-2\">\n <ProgressMeter\n value={pattern.averageProductivity}\n max={100}\n higherIsWorse={false}\n label=\"Productivity\"\n format={(v) => `${Math.round(v)}%`}\n size=\"md\"\n />\n {(pattern.mostProductiveDay || pattern.leastProductiveDay) && (\n <div className=\"flex flex-wrap items-center gap-2\">\n {pattern.mostProductiveDay && (\n <StatusBadge\n variant=\"success\"\n label={`Peak: ${pattern.mostProductiveDay}`}\n />\n )}\n {pattern.leastProductiveDay && (\n <StatusBadge\n variant=\"muted\"\n label={`Quietest: ${pattern.leastProductiveDay}`}\n />\n )}\n </div>\n )}\n </div>\n ),\n },\n ]\n : []),\n ...(focus\n ? [\n {\n id: \"focus-score\",\n name: \"Focus Score Report\",\n typeLabel: \"Productivity\",\n typeVariant: \"warning\" as const,\n period: `Avg ${Math.round(focus.averageScore)} · High ${Math.round(focus.highestScore)} · Low ${Math.round(focus.lowestScore)}`,\n icon: <Brain className=\"h-5 w-5\" />,\n accent: \"bg-status-warning-bg text-status-warning-text\",\n visual: (\n <div className=\"flex items-center gap-4\">\n <div className=\"shrink-0\">\n <RadialGauge\n value={focus.averageScore}\n min={0}\n max={100}\n label=\"Avg\"\n thresholds={{ warning: 75, danger: 50 }}\n height={128}\n className=\"w-32\"\n />\n </div>\n <div className=\"min-w-0 flex-1 space-y-1.5 text-xs\">\n <div className=\"flex items-center justify-between\">\n <span className=\"text-text-secondary\">Highest</span>\n <span className=\"font-semibold tabular-nums text-text-primary\">\n {Math.round(focus.highestScore)}\n </span>\n </div>\n <div className=\"flex items-center justify-between\">\n <span className=\"text-text-secondary\">Lowest</span>\n <span className=\"font-semibold tabular-nums text-text-primary\">\n {Math.round(focus.lowestScore)}\n </span>\n </div>\n {focus.trend && (\n <div className=\"flex items-center justify-between\">\n <span className=\"text-text-secondary\">Trend</span>\n <span className=\"font-medium capitalize text-text-primary\">\n {focus.trend}\n </span>\n </div>\n )}\n </div>\n </div>\n ),\n },\n ]\n : []),\n ];\n\n return (\n <PageLayout\n title=\"Reports\"\n description=\"Generate and view time tracking reports\"\n purpose=\"Package your tracked time into shareable, exportable reports — the deliverables you hand to clients for billing or to leadership for capacity and utilisation decisions. Reports build automatically from your data; refresh to pull the latest, and download any report as CSV.\"\n actions={\n <button\n onClick={handleRefresh}\n disabled={refreshing}\n className=\"inline-flex items-center gap-1.5 rounded-md bg-action-primary-bg px-3 py-2 text-sm font-medium text-action-primary-text hover:bg-action-primary-bg/90 disabled:opacity-70\"\n >\n <RefreshCw\n className={`h-4 w-4 ${refreshing ? \"motion-safe:animate-spin\" : \"\"}`}\n />\n {refreshing ? \"Refreshing...\" : \"Refresh\"}\n </button>\n }\n >\n {reports.length === 0 ? (\n <EmptyState\n illustration=\"empty-data\"\n title=\"No report data available yet\"\n description=\"Track some time first — once you have entries, focus scores or work patterns, ready-to-export reports appear here.\"\n />\n ) : (\n <div className=\"grid grid-cols-1 gap-4 xl:grid-cols-2\">\n {reports.map((report) => (\n <GlassCard\n key={report.id}\n treatment=\"glass\"\n className=\"flex flex-col p-4 sm:p-5\"\n >\n <div className=\"flex items-start justify-between gap-3\">\n <div className=\"flex min-w-0 items-start gap-3\">\n <div\n className={cn(\n \"flex h-10 w-10 shrink-0 items-center justify-center rounded-lg\",\n report.accent,\n )}\n >\n {report.icon}\n </div>\n <div className=\"min-w-0\">\n <div className=\"flex flex-wrap items-center gap-2\">\n <p className=\"text-sm font-semibold text-text-primary\">\n {report.name}\n </p>\n <StatusBadge\n variant={report.typeVariant}\n label={report.typeLabel}\n />\n </div>\n <p className=\"mt-1 text-xs text-text-secondary\">\n {report.period}\n </p>\n </div>\n </div>\n <div className=\"flex shrink-0 items-center gap-2\">\n <StatusBadge variant=\"success\" label=\"Ready\" dot />\n <button\n onClick={() => handleDownload(report.id)}\n title=\"Download as CSV\"\n aria-label={`Download ${report.name} as CSV`}\n className=\"inline-flex items-center gap-1 rounded-md border border-border-seam bg-bg-surface px-2 py-1.5 text-xs font-medium text-text-secondary hover:bg-bg-sunken\"\n >\n <Download className=\"h-4 w-4\" />\n CSV\n </button>\n </div>\n </div>\n <div className=\"mt-4 border-t border-border-subtle pt-4\">\n {report.visual}\n </div>\n </GlassCard>\n ))}\n </div>\n )}\n </PageLayout>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"ReportsPage.js","names":[],"sources":["../../../../src/timecampus/pages/dashboard/ReportsPage.tsx"],"sourcesContent":["import { useState, type ReactNode } from \"react\";\nimport {\n RefreshCw,\n Download,\n Clock,\n DollarSign,\n CalendarRange,\n Brain,\n} from \"lucide-react\";\nimport { GlassCard } from \"@burdenoff/fe-libs/ui\";\nimport { PageLayout } from \"../../components/PageLayout\";\nimport { EmptyState } from \"../../components/EmptyState\";\nimport { LoadingSkeleton } from \"../../components/LoadingSkeleton\";\nimport {\n StatusBadge,\n ProgressMeter,\n RadialGauge,\n BarsChart,\n} from \"../../components\";\nimport { cn, formatHours } from \"../../utils/helpers\";\n\n/** Trigger a client-side CSV download from an array of [key, value] rows. */\nfunction downloadCsv(filename: string, rows: Array<[string, string | number]>) {\n const escape = (v: string | number) => `\"${String(v).replace(/\"/g, '\"\"')}\"`;\n const csv = [\n \"Metric,Value\",\n ...rows.map(([k, v]) => `${escape(k)},${escape(v)}`),\n ].join(\"\\n\");\n const blob = new Blob([csv], { type: \"text/csv;charset=utf-8;\" });\n const url = URL.createObjectURL(blob);\n const link = document.createElement(\"a\");\n link.href = url;\n link.download = filename;\n document.body.appendChild(link);\n link.click();\n document.body.removeChild(link);\n URL.revokeObjectURL(url);\n}\nimport { useTimeCampus } from \"../../providers/TimeCampusProvider\";\nimport {\n useGetTimeEntryStatsQuery,\n useGetWorkPatternSummaryQuery,\n useGetFocusScoreSummaryQuery,\n} from \"@/generated/wspace-operations\";\n\ntype ReportTypeVariant = \"info\" | \"success\" | \"warning\" | \"default\";\n\ninterface ReportCard {\n id: string;\n name: string;\n typeLabel: string;\n typeVariant: ReportTypeVariant;\n period: string;\n icon: ReactNode;\n accent: string;\n visual: ReactNode;\n}\n\nexport function ReportsPage() {\n const { workspaceId = \"\" } = useTimeCampus();\n const [refreshing, setRefreshing] = useState(false);\n\n const {\n data: statsData,\n loading: statsLoading,\n error: statsError,\n refetch: refetchStats,\n } = useGetTimeEntryStatsQuery({\n skip: !workspaceId,\n });\n\n const {\n data: patternData,\n loading: patternLoading,\n refetch: refetchPattern,\n } = useGetWorkPatternSummaryQuery({\n skip: !workspaceId,\n });\n\n const {\n data: focusData,\n loading: focusLoading,\n refetch: refetchFocus,\n } = useGetFocusScoreSummaryQuery({\n skip: !workspaceId,\n });\n\n const stats = statsData?.timeEntryStats;\n const pattern = patternData?.workPatternSummary;\n const focus = focusData?.focusScoreSummary;\n\n const handleRefresh = async () => {\n setRefreshing(true);\n try {\n await Promise.all([refetchStats(), refetchPattern(), refetchFocus()]);\n } finally {\n setRefreshing(false);\n }\n };\n\n const handleDownload = (reportId: string) => {\n if (\n (reportId === \"time-summary\" || reportId === \"billable-summary\") &&\n stats\n ) {\n downloadCsv(`${reportId}.csv`, [\n [\"Total hours\", Math.round(stats.totalHours)],\n [\"Billable hours\", Math.round(stats.billableHours)],\n [\"Total entries\", stats.totalEntries],\n [\"Active timers\", stats.activeTimers],\n ]);\n } else if (reportId === \"work-pattern\" && pattern) {\n downloadCsv(\"work-pattern-analysis.csv\", [\n [\"Average focus hours\", pattern.averageFocusHours.toFixed(1)],\n [\"Average productivity %\", Math.round(pattern.averageProductivity)],\n ]);\n } else if (reportId === \"focus-score\" && focus) {\n downloadCsv(\"focus-score-report.csv\", [\n [\"Average score\", Math.round(focus.averageScore)],\n [\"Highest score\", Math.round(focus.highestScore)],\n [\"Lowest score\", Math.round(focus.lowestScore)],\n ]);\n }\n };\n\n const isLoading = statsLoading || patternLoading || focusLoading;\n\n if (isLoading) {\n return (\n <PageLayout\n title=\"Reports\"\n description=\"Generate and view time tracking reports\"\n actions={\n <button\n disabled\n className=\"inline-flex items-center gap-1.5 rounded-md bg-action-primary-bg px-3 py-2 text-sm font-medium text-action-primary-text opacity-70\"\n >\n <RefreshCw className=\"h-4 w-4\" />\n Refresh\n </button>\n }\n >\n <LoadingSkeleton variant=\"list\" count={4} />\n </PageLayout>\n );\n }\n\n if (statsError) {\n return (\n <PageLayout\n title=\"Reports\"\n description=\"Generate and view time tracking reports\"\n >\n <div className=\"rounded-lg border border-status-error-bg/40 bg-status-error-bg p-4 text-sm text-status-error-text\">\n Failed to load reports data. Please try again later.\n </div>\n </PageLayout>\n );\n }\n\n // Derived, real figures shared by the report visuals.\n const nonBillableHours = stats\n ? Math.max(0, stats.totalHours - stats.billableHours)\n : 0;\n const billablePct =\n stats && stats.totalHours > 0\n ? Math.round((stats.billableHours / stats.totalHours) * 100)\n : 0;\n\n // Build dynamic report cards from available analytics data. Each carries a\n // small, real visual fed only by the aggregate the page already fetched.\n const reports: ReportCard[] = [\n ...(stats\n ? [\n {\n id: \"time-summary\",\n name: \"Time Tracking Summary\",\n typeLabel: \"Time\",\n typeVariant: \"info\" as const,\n period: `${formatHours(stats.totalHours)} total · ${stats.totalEntries} entries`,\n icon: <Clock className=\"h-5 w-5\" />,\n accent: \"bg-status-info-bg text-status-info-text\",\n visual: (\n <div className=\"space-y-2\">\n <ProgressMeter\n value={stats.billableHours}\n max={stats.totalHours > 0 ? stats.totalHours : 1}\n higherIsWorse={false}\n label=\"Billable share\"\n format={formatHours}\n size=\"md\"\n />\n <div className=\"flex items-center justify-between text-xs text-text-secondary\">\n <span>{stats.totalEntries} entries logged</span>\n <span>\n {stats.activeTimers > 0\n ? `${stats.activeTimers} timer(s) running`\n : \"No active timers\"}\n </span>\n </div>\n </div>\n ),\n },\n {\n id: \"billable-summary\",\n name: \"Billable Hours Report\",\n typeLabel: \"Billing\",\n typeVariant: \"success\" as const,\n period: `${formatHours(stats.billableHours)} billable · ${billablePct}% of tracked`,\n icon: <DollarSign className=\"h-5 w-5\" />,\n accent: \"bg-status-success-bg text-status-success-text\",\n visual: (\n <BarsChart\n data={[\n { label: \"Billable\", hours: stats.billableHours },\n { label: \"Non-billable\", hours: nonBillableHours },\n ]}\n xKey=\"label\"\n series={[{ key: \"hours\", label: \"Hours\" }]}\n orientation=\"horizontal\"\n valueFormat={formatHours}\n categoryWidth={92}\n height={120}\n showGrid={false}\n ariaLabel=\"Billable versus non-billable hours\"\n />\n ),\n },\n ]\n : []),\n ...(pattern\n ? [\n {\n id: \"work-pattern\",\n name: \"Work Pattern Analysis\",\n typeLabel: \"Patterns\",\n typeVariant: \"default\" as const,\n period: `${formatHours(pattern.averageFocusHours)} avg focus · ${Math.round(pattern.averageProductivity)}% productivity`,\n icon: <CalendarRange className=\"h-5 w-5\" />,\n accent: \"bg-action-primary-bg/10 text-primary\",\n visual: (\n <div className=\"space-y-2\">\n <ProgressMeter\n value={pattern.averageProductivity}\n max={100}\n higherIsWorse={false}\n label=\"Productivity\"\n format={(v) => `${Math.round(v)}%`}\n size=\"md\"\n />\n {(pattern.mostProductiveDay || pattern.leastProductiveDay) && (\n <div className=\"flex flex-wrap items-center gap-2\">\n {pattern.mostProductiveDay && (\n <StatusBadge\n variant=\"success\"\n label={`Peak: ${pattern.mostProductiveDay}`}\n />\n )}\n {pattern.leastProductiveDay && (\n <StatusBadge\n variant=\"muted\"\n label={`Quietest: ${pattern.leastProductiveDay}`}\n />\n )}\n </div>\n )}\n </div>\n ),\n },\n ]\n : []),\n ...(focus\n ? [\n {\n id: \"focus-score\",\n name: \"Focus Score Report\",\n typeLabel: \"Productivity\",\n typeVariant: \"warning\" as const,\n period: `Avg ${Math.round(focus.averageScore)} · High ${Math.round(focus.highestScore)} · Low ${Math.round(focus.lowestScore)}`,\n icon: <Brain className=\"h-5 w-5\" />,\n accent: \"bg-status-warning-bg text-status-warning-text\",\n visual: (\n <div className=\"flex items-center gap-4\">\n <div className=\"shrink-0\">\n <RadialGauge\n value={focus.averageScore}\n min={0}\n max={100}\n label=\"Avg\"\n thresholds={{ warning: 75, danger: 50 }}\n height={128}\n className=\"w-32\"\n />\n </div>\n <div className=\"min-w-0 flex-1 space-y-1.5 text-xs\">\n <div className=\"flex items-center justify-between\">\n <span className=\"text-text-secondary\">Highest</span>\n <span className=\"font-semibold tabular-nums text-text-primary\">\n {Math.round(focus.highestScore)}\n </span>\n </div>\n <div className=\"flex items-center justify-between\">\n <span className=\"text-text-secondary\">Lowest</span>\n <span className=\"font-semibold tabular-nums text-text-primary\">\n {Math.round(focus.lowestScore)}\n </span>\n </div>\n {focus.trend && (\n <div className=\"flex items-center justify-between\">\n <span className=\"text-text-secondary\">Trend</span>\n <span className=\"font-medium capitalize text-text-primary\">\n {focus.trend}\n </span>\n </div>\n )}\n </div>\n </div>\n ),\n },\n ]\n : []),\n ];\n\n return (\n <PageLayout\n title=\"Reports\"\n description=\"Generate and view time tracking reports\"\n purpose=\"Package your tracked time into shareable, exportable reports — the deliverables you hand to clients for billing or to leadership for capacity and utilisation decisions. Reports build automatically from your data; refresh to pull the latest, and download any report as CSV.\"\n actions={\n <button\n onClick={handleRefresh}\n disabled={refreshing}\n className=\"inline-flex items-center gap-1.5 rounded-md bg-action-primary-bg px-3 py-2 text-sm font-medium text-action-primary-text hover:bg-action-primary-bg/90 disabled:opacity-70\"\n >\n <RefreshCw\n className={`h-4 w-4 ${refreshing ? \"motion-safe:animate-spin\" : \"\"}`}\n />\n {refreshing ? \"Refreshing...\" : \"Refresh\"}\n </button>\n }\n >\n {reports.length === 0 ? (\n <EmptyState\n illustration=\"empty-data\"\n title=\"No report data available yet\"\n description=\"Track some time first — once you have entries, focus scores or work patterns, ready-to-export reports appear here.\"\n />\n ) : (\n <div className=\"grid grid-cols-1 gap-4 xl:grid-cols-2\">\n {reports.map((report) => (\n <GlassCard\n key={report.id}\n treatment=\"glass\"\n className=\"flex flex-col p-4 sm:p-5\"\n >\n <div className=\"flex items-start justify-between gap-3\">\n <div className=\"flex min-w-0 items-start gap-3\">\n <div\n className={cn(\n \"flex h-10 w-10 shrink-0 items-center justify-center rounded-lg\",\n report.accent,\n )}\n >\n {report.icon}\n </div>\n <div className=\"min-w-0\">\n <div className=\"flex flex-wrap items-center gap-2\">\n <p className=\"text-sm font-semibold text-text-primary\">\n {report.name}\n </p>\n <StatusBadge\n variant={report.typeVariant}\n label={report.typeLabel}\n />\n </div>\n <p className=\"mt-1 text-xs text-text-secondary\">\n {report.period}\n </p>\n </div>\n </div>\n <div className=\"flex shrink-0 items-center gap-2\">\n <StatusBadge variant=\"success\" label=\"Ready\" dot />\n <button\n onClick={() => handleDownload(report.id)}\n title=\"Download as CSV\"\n aria-label={`Download ${report.name} as CSV`}\n className=\"inline-flex items-center gap-1 rounded-md border border-border-seam bg-bg-surface px-2 py-1.5 text-xs font-medium text-text-secondary hover:bg-bg-sunken\"\n >\n <Download className=\"h-4 w-4\" />\n CSV\n </button>\n </div>\n </div>\n <div className=\"mt-4 border-t border-border-subtle pt-4\">\n {report.visual}\n </div>\n </GlassCard>\n ))}\n </div>\n )}\n </PageLayout>\n );\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"EmailAnalyticsPage.js","names":[],"sources":["../../../../src/timecampus/pages/email-tracking/EmailAnalyticsPage.tsx"],"sourcesContent":["import { BarChart3, Mail, Clock } from \"lucide-react\";\nimport { PageLayout } from \"../../components/PageLayout\";\nimport { StatCard } from \"../../components/StatCard\";\nimport { LoadingSkeleton } from \"../../components/LoadingSkeleton\";\nimport {\n ChartCard,\n BarsChart,\n DonutChart,\n type DonutSlice,\n type ValueFormatter,\n} from \"../../components\";\nimport { useTimeCampus } from \"../../providers/TimeCampusProvider\";\nimport { useGetEmailTrackingStatsQuery } from \"@/generated/wspace-operations\";\n\n/** Email `duration` values are stored in whole minutes across the product. */\nconst formatMinutes: ValueFormatter = (value) => {\n const total = Math.max(0, Math.round(value));\n const h = Math.floor(total / 60);\n const m = total % 60;\n return h > 0 ? `${h}h ${m}m` : `${m}m`;\n};\n\ninterface CategoryStat {\n category: string;\n label: string;\n count: number;\n minutes: number;\n}\n\n/** Safely narrow the JSON `byCategory` map into typed `{ count, totalDuration }` rows. */\nfunction parseByCategory(\n raw: Record<string, unknown> | null | undefined,\n): CategoryStat[] {\n if (!raw) return [];\n const rows: CategoryStat[] = [];\n for (const [category, value] of Object.entries(raw)) {\n if (value && typeof value === \"object\") {\n const rec = value as { count?: unknown; totalDuration?: unknown };\n const count = typeof rec.count === \"number\" ? rec.count : 0;\n const minutes =\n typeof rec.totalDuration === \"number\" ? rec.totalDuration : 0;\n rows.push({\n category,\n label: category.charAt(0).toUpperCase() + category.slice(1),\n count,\n minutes,\n });\n }\n }\n return rows.sort((a, b) => b.minutes - a.minutes);\n}\n\nexport function EmailAnalyticsPage() {\n const { workspaceId = \"\" } = useTimeCampus();\n const { data, loading, error } = useGetEmailTrackingStatsQuery({\n skip: !workspaceId,\n });\n\n const stats = data?.emailTrackingStats;\n\n if (loading) {\n return (\n <PageLayout\n title=\"Email Analytics\"\n description=\"Insights into email time management\"\n >\n <LoadingSkeleton variant=\"card\" count={4} />\n </PageLayout>\n );\n }\n\n if (error) {\n return (\n <PageLayout\n title=\"Email Analytics\"\n description=\"Insights into email time management\"\n >\n <div className=\"rounded-xl border border-status-error-border/50 bg-status-error-bg/10 p-4 text-sm text-status-error-text\">\n Failed to load email analytics. Please try again later.\n </div>\n </PageLayout>\n );\n }\n\n const totalMinutes = Math.round((stats?.totalDuration ?? 0) / 60);\n const hours = Math.floor(totalMinutes / 60);\n const minutes = totalMinutes % 60;\n const timeOnEmail = hours > 0 ? `${hours}h ${minutes}m` : `${minutes}m`;\n const avgResponse =\n stats?.averageResponseTime != null\n ? `${Math.round(stats.averageResponseTime)} min`\n : \"--\";\n const totalEmails = stats?.totalEmails ?? 0;\n const importantCount = stats?.importantCount ?? 0;\n const importantRatio = totalEmails\n ? Math.round((importantCount / totalEmails) * 100)\n : 0;\n\n const categoryStats = parseByCategory(stats?.byCategory);\n const categoryBars = categoryStats.map((row) => ({\n category: row.label,\n minutes: row.minutes,\n }));\n\n const regularCount = Math.max(0, totalEmails - importantCount);\n const compositionSlices: DonutSlice[] = [\n { name: \"Important\", value: importantCount },\n { name: \"Regular\", value: regularCount },\n ];\n\n return (\n <PageLayout\n title=\"Email Analytics\"\n description=\"Insights into email time management\"\n purpose=\"Know exactly how much of your day email consumes and how fast you respond — so you can set realistic response targets, batch your inbox, and stop email from quietly running your schedule.\"\n >\n <div className=\"space-y-6\">\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4\">\n <StatCard\n featured\n title=\"Emails Today\"\n value={String(totalEmails)}\n icon={<Mail className=\"h-5 w-5\" />}\n />\n <StatCard\n title=\"Time on Email\"\n value={timeOnEmail}\n icon={<Clock className=\"h-5 w-5\" />}\n />\n <StatCard\n title=\"Avg. Response\"\n value={avgResponse}\n description=\"Target: 15 min\"\n />\n <StatCard\n title=\"Important Share\"\n value={`${importantRatio}%`}\n icon={<BarChart3 className=\"h-5 w-5\" />}\n description={`${importantCount} of ${totalEmails} flagged`}\n />\n </div>\n\n <div className=\"grid grid-cols-1 gap-4 lg:grid-cols-2\">\n <ChartCard\n title=\"Time on Email by Category\"\n description=\"How your inbox time splits across categories\"\n >\n <BarsChart\n data={categoryBars}\n xKey=\"category\"\n orientation=\"horizontal\"\n series={[{ key: \"minutes\", label: \"Time on email\" }]}\n valueFormat={formatMinutes}\n categoryWidth={120}\n height={280}\n emptyLabel=\"No categorized email time tracked yet\"\n />\n </ChartCard>\n\n <ChartCard\n title=\"Email Composition\"\n description=\"Important vs. regular messages\"\n >\n <DonutChart\n data={compositionSlices}\n centerLabel=\"Emails tracked\"\n centerValue={totalEmails}\n height={280}\n emptyLabel=\"No emails tracked yet\"\n />\n </ChartCard>\n </div>\n </div>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"EmailAnalyticsPage.js","names":[],"sources":["../../../../src/timecampus/pages/email-tracking/EmailAnalyticsPage.tsx"],"sourcesContent":["import { BarChart3, Mail, Clock } from \"lucide-react\";\nimport { PageLayout } from \"../../components/PageLayout\";\nimport { StatCard } from \"../../components/StatCard\";\nimport { LoadingSkeleton } from \"../../components/LoadingSkeleton\";\nimport {\n ChartCard,\n BarsChart,\n DonutChart,\n type DonutSlice,\n type ValueFormatter,\n} from \"../../components\";\nimport { useTimeCampus } from \"../../providers/TimeCampusProvider\";\nimport { useGetEmailTrackingStatsQuery } from \"@/generated/wspace-operations\";\n\n/** Email `duration` values are stored in whole minutes across the product. */\nconst formatMinutes: ValueFormatter = (value) => {\n const total = Math.max(0, Math.round(value));\n const h = Math.floor(total / 60);\n const m = total % 60;\n return h > 0 ? `${h}h ${m}m` : `${m}m`;\n};\n\ninterface CategoryStat {\n category: string;\n label: string;\n count: number;\n minutes: number;\n}\n\n/** Safely narrow the JSON `byCategory` map into typed `{ count, totalDuration }` rows. */\nfunction parseByCategory(\n raw: Record<string, unknown> | null | undefined,\n): CategoryStat[] {\n if (!raw) return [];\n const rows: CategoryStat[] = [];\n for (const [category, value] of Object.entries(raw)) {\n if (value && typeof value === \"object\") {\n const rec = value as { count?: unknown; totalDuration?: unknown };\n const count = typeof rec.count === \"number\" ? rec.count : 0;\n const minutes =\n typeof rec.totalDuration === \"number\" ? rec.totalDuration : 0;\n rows.push({\n category,\n label: category.charAt(0).toUpperCase() + category.slice(1),\n count,\n minutes,\n });\n }\n }\n return rows.sort((a, b) => b.minutes - a.minutes);\n}\n\nexport function EmailAnalyticsPage() {\n const { workspaceId = \"\" } = useTimeCampus();\n const { data, loading, error } = useGetEmailTrackingStatsQuery({\n skip: !workspaceId,\n });\n\n const stats = data?.emailTrackingStats;\n\n if (loading) {\n return (\n <PageLayout\n title=\"Email Analytics\"\n description=\"Insights into email time management\"\n >\n <LoadingSkeleton variant=\"card\" count={4} />\n </PageLayout>\n );\n }\n\n if (error) {\n return (\n <PageLayout\n title=\"Email Analytics\"\n description=\"Insights into email time management\"\n >\n <div className=\"rounded-xl border border-status-error-border/50 bg-status-error-bg/10 p-4 text-sm text-status-error-text\">\n Failed to load email analytics. Please try again later.\n </div>\n </PageLayout>\n );\n }\n\n const totalMinutes = Math.round((stats?.totalDuration ?? 0) / 60);\n const hours = Math.floor(totalMinutes / 60);\n const minutes = totalMinutes % 60;\n const timeOnEmail = hours > 0 ? `${hours}h ${minutes}m` : `${minutes}m`;\n const avgResponse =\n stats?.averageResponseTime != null\n ? `${Math.round(stats.averageResponseTime)} min`\n : \"--\";\n const totalEmails = stats?.totalEmails ?? 0;\n const importantCount = stats?.importantCount ?? 0;\n const importantRatio = totalEmails\n ? Math.round((importantCount / totalEmails) * 100)\n : 0;\n\n const categoryStats = parseByCategory(stats?.byCategory);\n const categoryBars = categoryStats.map((row) => ({\n category: row.label,\n minutes: row.minutes,\n }));\n\n const regularCount = Math.max(0, totalEmails - importantCount);\n const compositionSlices: DonutSlice[] = [\n { name: \"Important\", value: importantCount },\n { name: \"Regular\", value: regularCount },\n ];\n\n return (\n <PageLayout\n title=\"Email Analytics\"\n description=\"Insights into email time management\"\n purpose=\"Know exactly how much of your day email consumes and how fast you respond — so you can set realistic response targets, batch your inbox, and stop email from quietly running your schedule.\"\n >\n <div className=\"space-y-6\">\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4\">\n <StatCard\n featured\n title=\"Emails Today\"\n value={String(totalEmails)}\n icon={<Mail className=\"h-5 w-5\" />}\n />\n <StatCard\n title=\"Time on Email\"\n value={timeOnEmail}\n icon={<Clock className=\"h-5 w-5\" />}\n />\n <StatCard\n title=\"Avg. Response\"\n value={avgResponse}\n description=\"Target: 15 min\"\n />\n <StatCard\n title=\"Important Share\"\n value={`${importantRatio}%`}\n icon={<BarChart3 className=\"h-5 w-5\" />}\n description={`${importantCount} of ${totalEmails} flagged`}\n />\n </div>\n\n <div className=\"grid grid-cols-1 gap-4 lg:grid-cols-2\">\n <ChartCard\n title=\"Time on Email by Category\"\n description=\"How your inbox time splits across categories\"\n >\n <BarsChart\n data={categoryBars}\n xKey=\"category\"\n orientation=\"horizontal\"\n series={[{ key: \"minutes\", label: \"Time on email\" }]}\n valueFormat={formatMinutes}\n categoryWidth={120}\n height={280}\n emptyLabel=\"No categorized email time tracked yet\"\n />\n </ChartCard>\n\n <ChartCard\n title=\"Email Composition\"\n description=\"Important vs. regular messages\"\n >\n <DonutChart\n data={compositionSlices}\n centerLabel=\"Emails tracked\"\n centerValue={totalEmails}\n height={280}\n emptyLabel=\"No emails tracked yet\"\n />\n </ChartCard>\n </div>\n </div>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"EmailTrackingPage.d.ts","sourceRoot":"","sources":["../../../../src/timecampus/pages/email-tracking/EmailTrackingPage.tsx"],"names":[],"mappings":"AA8LA,wBAAgB,iBAAiB,gCAkVhC"}
1
+ {"version":3,"file":"EmailTrackingPage.d.ts","sourceRoot":"","sources":["../../../../src/timecampus/pages/email-tracking/EmailTrackingPage.tsx"],"names":[],"mappings":"AA+LA,wBAAgB,iBAAiB,gCAqVhC"}