@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":"TimerPage.js","names":[],"sources":["../../../../src/timecampus/pages/time/TimerPage.tsx"],"sourcesContent":["import { useEffect, useState } from \"react\";\nimport { Play, Square, RotateCcw } from \"lucide-react\";\nimport { GlassCard, NextSteps } from \"@burdenoff/fe-libs/ui\";\nimport { PageLayout } from \"../../components/PageLayout\";\nimport { Modal } from \"../../components/Modal\";\nimport { EmptyState } from \"../../components/EmptyState\";\nimport { LoadingSkeleton } from \"../../components/LoadingSkeleton\";\nimport { StatusBadge } from \"../../components/StatusBadge\";\nimport { useTimeCampusNavigate } from \"../../providers/TimeCampusProvider\";\nimport { formatTimerDuration, getInitials } from \"../../utils/helpers\";\nimport { useTimeCampusStore } from \"../../store/timecampusStore\";\nimport {\n useListRunningTimersQuery,\n useStartTimerMutation,\n useStopTimerMutation,\n useDeleteTimeEntryMutation,\n useListProjectsQuery,\n ListRunningTimersDocument,\n ListTimeEntriesDocument,\n GetTimeEntryStatsDocument,\n type ListRunningTimersQuery,\n type ListProjectsQuery,\n} from \"@/generated/wspace-operations\";\n\ntype RunningTimerItem = ListRunningTimersQuery[\"runningTimers\"][number];\ntype ProjectOption = ListProjectsQuery[\"projects\"][\"items\"][number];\n\nexport function TimerPage() {\n const { activeTimer, tickTimer, syncTimerFromServer } = useTimeCampusStore();\n const navigate = useTimeCampusNavigate();\n\n const [startOpen, setStartOpen] = useState(false);\n const [description, setDescription] = useState(\"\");\n const [projectId, setProjectId] = useState(\"\");\n const [billable, setBillable] = useState(false);\n const [resetTarget, setResetTarget] = useState<RunningTimerItem | null>(null);\n\n const {\n data: runningData,\n loading: runningLoading,\n refetch: refetchRunning,\n } = useListRunningTimersQuery({ fetchPolicy: \"cache-and-network\" });\n\n const { data: projectsData } = useListProjectsQuery({\n variables: { pagination: { limit: 50 } },\n });\n const projects: ProjectOption[] = projectsData?.projects?.items ?? [];\n\n const runningTimers = runningData?.runningTimers ?? [];\n const serverTimer = runningTimers[0] ?? null;\n\n const refetchAll = [\n { query: ListRunningTimersDocument },\n {\n query: ListTimeEntriesDocument,\n variables: { pagination: { limit: 50 } },\n },\n { query: GetTimeEntryStatsDocument },\n ];\n\n const [startTimerMutation, { loading: starting, error: startError }] =\n useStartTimerMutation({ refetchQueries: refetchAll });\n const [stopTimerMutation, { loading: stopping }] = useStopTimerMutation({\n refetchQueries: refetchAll,\n });\n const [deleteTimeEntry, { loading: deleting }] = useDeleteTimeEntryMutation({\n refetchQueries: refetchAll,\n });\n\n // Backend is the source of truth for whether a timer is running — mirror the\n // server running timer into the local display store whenever it changes.\n useEffect(() => {\n if (serverTimer) {\n syncTimerFromServer({\n timeEntryId: serverTimer.timeEntry.id,\n elapsedSeconds: serverTimer.elapsedSeconds,\n projectId: serverTimer.timeEntry.projectId ?? undefined,\n taskId: serverTimer.timeEntry.taskId ?? undefined,\n description: serverTimer.timeEntry.description ?? undefined,\n });\n } else if (!runningLoading) {\n syncTimerFromServer(null);\n }\n // Only re-sync when the server's running timer identity/elapsed changes.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [serverTimer?.timeEntry.id, serverTimer?.elapsedSeconds, runningLoading]);\n\n // Local ticking display for the live seconds counter.\n useEffect(() => {\n if (!activeTimer?.isRunning) return;\n const interval = setInterval(() => tickTimer(), 1000);\n return () => clearInterval(interval);\n }, [activeTimer?.isRunning, tickTimer]);\n\n const handleStart = async () => {\n await startTimerMutation({\n variables: {\n input: {\n description: description.trim() || undefined,\n projectId: projectId || undefined,\n billable,\n source: \"timer\",\n },\n },\n });\n setStartOpen(false);\n setDescription(\"\");\n setProjectId(\"\");\n setBillable(false);\n await refetchRunning();\n };\n\n const handleStop = async () => {\n if (!serverTimer) return;\n await stopTimerMutation({ variables: { id: serverTimer.timeEntry.id } });\n await refetchRunning();\n };\n\n const handleReset = async () => {\n if (!resetTarget) return;\n // Discard the in-progress session: stop it, then delete the entry.\n await stopTimerMutation({ variables: { id: resetTarget.timeEntry.id } });\n await deleteTimeEntry({ variables: { id: resetTarget.timeEntry.id } });\n setResetTarget(null);\n await refetchRunning();\n };\n\n const isRunning = Boolean(serverTimer);\n\n return (\n <PageLayout\n title=\"Timer\"\n description=\"Focus timer for deep work sessions\"\n purpose=\"A single, distraction-free stopwatch for the task in front of you — start it when you begin focused work and it logs the session as a time entry you can attribute to a project later.\"\n >\n <div className=\"mx-auto max-w-lg\">\n <NextSteps\n storageKey=\"timecampus-timer\"\n className=\"mb-6\"\n steps={[\n {\n id: \"start\",\n label: isRunning ? \"Stop the running timer\" : \"Start the timer\",\n description: isRunning\n ? \"End this session and save it as a time entry.\"\n : \"Begin tracking the task you're working on right now.\",\n onClick: () => (isRunning ? handleStop() : setStartOpen(true)),\n },\n {\n id: \"assign-project\",\n label: \"Assign work to a project\",\n description: \"Pick a project so this time bills correctly.\",\n onClick: () => navigate(\"/projects\"),\n },\n {\n id: \"review-entries\",\n label: \"Review your time entries\",\n description: \"Check and tidy everything you've logged.\",\n onClick: () => navigate(\"/time/entries\"),\n },\n ]}\n />\n <GlassCard treatment=\"glass\" glow className=\"p-6 text-center sm:p-8\">\n <p className=\"font-mono text-5xl font-bold tabular-nums text-text-primary sm:text-6xl\">\n {activeTimer\n ? formatTimerDuration(activeTimer.seconds)\n : \"00:00:00\"}\n </p>\n {activeTimer?.description && (\n <p className=\"mt-2 text-sm text-text-secondary\">\n {activeTimer.description}\n </p>\n )}\n <div className=\"mt-6 flex flex-wrap items-center justify-center gap-3\">\n {isRunning ? (\n <button\n onClick={handleStop}\n disabled={stopping}\n className=\"inline-flex items-center gap-2 rounded-md bg-action-danger-bg px-6 py-3 text-sm font-medium text-action-primary-text hover:bg-action-danger-bg/90 disabled:opacity-50\"\n >\n <Square className=\"h-4 w-4\" />\n {stopping ? \"Stopping...\" : \"Stop\"}\n </button>\n ) : (\n <button\n onClick={() => setStartOpen(true)}\n disabled={starting}\n className=\"inline-flex items-center gap-2 rounded-md bg-action-primary-bg px-6 py-3 text-sm font-medium text-action-primary-text hover:bg-action-primary-bg/90 disabled:opacity-50\"\n >\n <Play className=\"h-4 w-4\" />\n Start\n </button>\n )}\n <button\n onClick={() => serverTimer && setResetTarget(serverTimer)}\n disabled={!isRunning || deleting}\n className=\"inline-flex items-center gap-2 rounded-md border border-border-subtle px-4 py-3 text-sm font-medium text-text-primary hover:bg-bg-sunken/50 disabled:opacity-50\"\n title=\"Discard the running timer without saving\"\n >\n <RotateCcw className=\"h-4 w-4\" />\n Reset\n </button>\n </div>\n </GlassCard>\n\n {/* Server-side running timers */}\n <GlassCard treatment=\"glass\" className=\"mt-6 p-6\">\n <div className=\"mb-4 flex items-center justify-between\">\n <h3 className=\"text-sm font-semibold text-text-primary\">\n Running Timers\n </h3>\n {runningTimers.length > 0 && (\n <StatusBadge\n label={`${runningTimers.length} active`}\n variant=\"success\"\n dot\n />\n )}\n </div>\n {runningLoading && runningTimers.length === 0 ? (\n <LoadingSkeleton variant=\"list\" count={2} />\n ) : runningTimers.length === 0 ? (\n <EmptyState\n illustration=\"empty-sessions\"\n title=\"No active timers on the server\"\n description=\"Timers you start sync here so you can pick them up on any device.\"\n />\n ) : (\n <div className=\"flex flex-col gap-2\">\n {runningTimers.map((timer: RunningTimerItem) => (\n <div\n key={timer.timeEntry.id}\n className=\"flex items-center gap-3 rounded-lg border border-border-subtle bg-bg-surface p-3\"\n >\n <span className=\"flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-action-primary-bg/10 text-xs font-semibold text-primary\">\n {getInitials(timer.timeEntry.project?.name ?? \"No Project\")}\n </span>\n <div className=\"min-w-0 flex-1\">\n <p className=\"truncate text-sm font-medium text-text-primary\">\n {timer.timeEntry.project?.name ?? \"No Project\"}\n </p>\n <p className=\"truncate text-xs text-text-secondary\">\n {timer.timeEntry.description ?? \"No description\"}\n </p>\n </div>\n <p className=\"ml-2 shrink-0 font-mono text-sm font-medium text-text-primary\">\n {formatTimerDuration(timer.elapsedSeconds)}\n </p>\n </div>\n ))}\n </div>\n )}\n </GlassCard>\n </div>\n\n {/* Start timer modal */}\n <Modal\n open={startOpen}\n onClose={() => setStartOpen(false)}\n title=\"Start a timer\"\n description=\"Track the task you're working on right now.\"\n footer={\n <>\n <button\n onClick={() => setStartOpen(false)}\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={handleStart}\n disabled={starting}\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 {starting ? \"Starting...\" : \"Start Timer\"}\n </button>\n </>\n }\n >\n {startError && (\n <div className=\"mb-4 rounded-lg border border-status-error-bg/40 bg-status-error-bg p-3 text-sm text-status-error-text\">\n Failed to start the timer. Please try again.\n </div>\n )}\n <div className=\"space-y-4\">\n <div>\n <label className=\"block text-sm font-medium text-text-primary\">\n What are you working on?\n </label>\n <input\n type=\"text\"\n placeholder=\"Description\"\n value={description}\n onChange={(e) => setDescription(e.target.value)}\n className=\"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 />\n </div>\n <div>\n <label className=\"block text-sm font-medium text-text-primary\">\n Project\n </label>\n <select\n value={projectId}\n onChange={(e) => setProjectId(e.target.value)}\n className=\"mt-1 w-full rounded-md border border-border-subtle bg-bg-surface px-3 py-2 text-sm text-text-primary\"\n >\n <option value=\"\">No Project</option>\n {projects.map((project) => (\n <option key={project.id} value={project.id}>\n {project.name}\n </option>\n ))}\n </select>\n </div>\n <label className=\"flex items-center gap-2 text-sm text-text-primary\">\n <input\n type=\"checkbox\"\n checked={billable}\n onChange={(e) => setBillable(e.target.checked)}\n className=\"h-4 w-4 rounded border-border-subtle\"\n />\n Billable\n </label>\n </div>\n </Modal>\n\n {/* Reset / discard confirm modal */}\n <Modal\n open={Boolean(resetTarget)}\n onClose={() => setResetTarget(null)}\n title=\"Discard running timer?\"\n description=\"This stops the timer and deletes the in-progress entry. It cannot be undone.\"\n footer={\n <>\n <button\n onClick={() => setResetTarget(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={handleReset}\n disabled={stopping || deleting}\n className=\"rounded-md bg-action-danger-bg px-4 py-2 text-sm font-medium text-action-primary-text hover:bg-action-danger-bg/90 disabled:opacity-50\"\n >\n {stopping || deleting ? \"Discarding...\" : \"Discard\"}\n </button>\n </>\n }\n >\n <p className=\"text-sm text-text-secondary\">\n The timer for{\" \"}\n <span className=\"font-medium text-text-primary\">\n {resetTarget?.timeEntry.project?.name ??\n resetTarget?.timeEntry.description ??\n \"this session\"}\n </span>{\" \"}\n will be discarded.\n </p>\n </Modal>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"TimerPage.js","names":[],"sources":["../../../../src/timecampus/pages/time/TimerPage.tsx"],"sourcesContent":["import { useEffect, useState } from \"react\";\nimport { Play, Square, RotateCcw } from \"lucide-react\";\nimport { GlassCard, NextSteps } from \"@burdenoff/fe-libs/ui\";\nimport { PageLayout } from \"../../components/PageLayout\";\nimport { Modal } from \"../../components/Modal\";\nimport { EmptyState } from \"../../components/EmptyState\";\nimport { LoadingSkeleton } from \"../../components/LoadingSkeleton\";\nimport { StatusBadge } from \"../../components/StatusBadge\";\nimport { useTimeCampusNavigate } from \"../../providers/TimeCampusProvider\";\nimport { formatTimerDuration, getInitials } from \"../../utils/helpers\";\nimport { useTimeCampusStore } from \"../../store/timecampusStore\";\nimport {\n useListRunningTimersQuery,\n useStartTimerMutation,\n useStopTimerMutation,\n useDeleteTimeEntryMutation,\n useListProjectsQuery,\n ListRunningTimersDocument,\n ListTimeEntriesDocument,\n GetTimeEntryStatsDocument,\n type ListRunningTimersQuery,\n type ListProjectsQuery,\n} from \"@/generated/wspace-operations\";\n\ntype RunningTimerItem = ListRunningTimersQuery[\"runningTimers\"][number];\ntype ProjectOption = ListProjectsQuery[\"projects\"][\"items\"][number];\n\nexport function TimerPage() {\n const { activeTimer, tickTimer, syncTimerFromServer } = useTimeCampusStore();\n const navigate = useTimeCampusNavigate();\n\n const [startOpen, setStartOpen] = useState(false);\n const [description, setDescription] = useState(\"\");\n const [projectId, setProjectId] = useState(\"\");\n const [billable, setBillable] = useState(false);\n const [resetTarget, setResetTarget] = useState<RunningTimerItem | null>(null);\n\n const {\n data: runningData,\n loading: runningLoading,\n refetch: refetchRunning,\n } = useListRunningTimersQuery({ fetchPolicy: \"cache-and-network\" });\n\n const { data: projectsData } = useListProjectsQuery({\n variables: { pagination: { limit: 50 } },\n });\n const projects: ProjectOption[] = projectsData?.projects?.items ?? [];\n\n const runningTimers = runningData?.runningTimers ?? [];\n const serverTimer = runningTimers[0] ?? null;\n\n const refetchAll = [\n { query: ListRunningTimersDocument },\n {\n query: ListTimeEntriesDocument,\n variables: { pagination: { limit: 50 } },\n },\n { query: GetTimeEntryStatsDocument },\n ];\n\n const [startTimerMutation, { loading: starting, error: startError }] =\n useStartTimerMutation({ refetchQueries: refetchAll });\n const [stopTimerMutation, { loading: stopping }] = useStopTimerMutation({\n refetchQueries: refetchAll,\n });\n const [deleteTimeEntry, { loading: deleting }] = useDeleteTimeEntryMutation({\n refetchQueries: refetchAll,\n });\n\n // Backend is the source of truth for whether a timer is running — mirror the\n // server running timer into the local display store whenever it changes.\n useEffect(() => {\n if (serverTimer) {\n syncTimerFromServer({\n timeEntryId: serverTimer.timeEntry.id,\n elapsedSeconds: serverTimer.elapsedSeconds,\n projectId: serverTimer.timeEntry.projectId ?? undefined,\n taskId: serverTimer.timeEntry.taskId ?? undefined,\n description: serverTimer.timeEntry.description ?? undefined,\n });\n } else if (!runningLoading) {\n syncTimerFromServer(null);\n }\n // Only re-sync when the server's running timer identity/elapsed changes.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [serverTimer?.timeEntry.id, serverTimer?.elapsedSeconds, runningLoading]);\n\n // Local ticking display for the live seconds counter.\n useEffect(() => {\n if (!activeTimer?.isRunning) return;\n const interval = setInterval(() => tickTimer(), 1000);\n return () => clearInterval(interval);\n }, [activeTimer?.isRunning, tickTimer]);\n\n const handleStart = async () => {\n await startTimerMutation({\n variables: {\n input: {\n description: description.trim() || undefined,\n projectId: projectId || undefined,\n billable,\n source: \"timer\",\n },\n },\n });\n setStartOpen(false);\n setDescription(\"\");\n setProjectId(\"\");\n setBillable(false);\n await refetchRunning();\n };\n\n const handleStop = async () => {\n if (!serverTimer) return;\n await stopTimerMutation({ variables: { id: serverTimer.timeEntry.id } });\n await refetchRunning();\n };\n\n const handleReset = async () => {\n if (!resetTarget) return;\n // Discard the in-progress session: stop it, then delete the entry.\n await stopTimerMutation({ variables: { id: resetTarget.timeEntry.id } });\n await deleteTimeEntry({ variables: { id: resetTarget.timeEntry.id } });\n setResetTarget(null);\n await refetchRunning();\n };\n\n const isRunning = Boolean(serverTimer);\n\n return (\n <PageLayout\n title=\"Timer\"\n description=\"Focus timer for deep work sessions\"\n purpose=\"A single, distraction-free stopwatch for the task in front of you — start it when you begin focused work and it logs the session as a time entry you can attribute to a project later.\"\n >\n <div className=\"mx-auto max-w-lg\">\n <NextSteps\n storageKey=\"timecampus-timer\"\n className=\"mb-6\"\n steps={[\n {\n id: \"start\",\n label: isRunning ? \"Stop the running timer\" : \"Start the timer\",\n description: isRunning\n ? \"End this session and save it as a time entry.\"\n : \"Begin tracking the task you're working on right now.\",\n onClick: () => (isRunning ? handleStop() : setStartOpen(true)),\n },\n {\n id: \"assign-project\",\n label: \"Assign work to a project\",\n description: \"Pick a project so this time bills correctly.\",\n onClick: () => navigate(\"/projects\"),\n },\n {\n id: \"review-entries\",\n label: \"Review your time entries\",\n description: \"Check and tidy everything you've logged.\",\n onClick: () => navigate(\"/time/entries\"),\n },\n ]}\n />\n <GlassCard treatment=\"glass\" glow className=\"p-6 text-center sm:p-8\">\n <p className=\"font-mono text-5xl font-bold tabular-nums text-text-primary sm:text-6xl\">\n {activeTimer\n ? formatTimerDuration(activeTimer.seconds)\n : \"00:00:00\"}\n </p>\n {activeTimer?.description && (\n <p className=\"mt-2 text-sm text-text-secondary\">\n {activeTimer.description}\n </p>\n )}\n <div className=\"mt-6 flex flex-wrap items-center justify-center gap-3\">\n {isRunning ? (\n <button\n onClick={handleStop}\n disabled={stopping}\n className=\"inline-flex items-center gap-2 rounded-md bg-action-danger-bg px-6 py-3 text-sm font-medium text-action-primary-text hover:bg-action-danger-bg/90 disabled:opacity-50\"\n >\n <Square className=\"h-4 w-4\" />\n {stopping ? \"Stopping...\" : \"Stop\"}\n </button>\n ) : (\n <button\n onClick={() => setStartOpen(true)}\n disabled={starting}\n className=\"inline-flex items-center gap-2 rounded-md bg-action-primary-bg px-6 py-3 text-sm font-medium text-action-primary-text hover:bg-action-primary-bg/90 disabled:opacity-50\"\n >\n <Play className=\"h-4 w-4\" />\n Start\n </button>\n )}\n <button\n onClick={() => serverTimer && setResetTarget(serverTimer)}\n disabled={!isRunning || deleting}\n className=\"inline-flex items-center gap-2 rounded-md border border-border-subtle px-4 py-3 text-sm font-medium text-text-primary hover:bg-bg-sunken/50 disabled:opacity-50\"\n title=\"Discard the running timer without saving\"\n >\n <RotateCcw className=\"h-4 w-4\" />\n Reset\n </button>\n </div>\n </GlassCard>\n\n {/* Server-side running timers */}\n <GlassCard treatment=\"glass\" className=\"mt-6 p-6\">\n <div className=\"mb-4 flex items-center justify-between\">\n <h3 className=\"text-sm font-semibold text-text-primary\">\n Running Timers\n </h3>\n {runningTimers.length > 0 && (\n <StatusBadge\n label={`${runningTimers.length} active`}\n variant=\"success\"\n dot\n />\n )}\n </div>\n {runningLoading && runningTimers.length === 0 ? (\n <LoadingSkeleton variant=\"list\" count={2} />\n ) : runningTimers.length === 0 ? (\n <EmptyState\n illustration=\"empty-sessions\"\n title=\"No active timers on the server\"\n description=\"Timers you start sync here so you can pick them up on any device.\"\n />\n ) : (\n <div className=\"flex flex-col gap-2\">\n {runningTimers.map((timer: RunningTimerItem) => (\n <div\n key={timer.timeEntry.id}\n className=\"flex items-center gap-3 rounded-lg border border-border-subtle bg-bg-surface p-3\"\n >\n <span className=\"flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-action-primary-bg/10 text-xs font-semibold text-primary\">\n {getInitials(timer.timeEntry.project?.name ?? \"No Project\")}\n </span>\n <div className=\"min-w-0 flex-1\">\n <p className=\"truncate text-sm font-medium text-text-primary\">\n {timer.timeEntry.project?.name ?? \"No Project\"}\n </p>\n <p className=\"truncate text-xs text-text-secondary\">\n {timer.timeEntry.description ?? \"No description\"}\n </p>\n </div>\n <p className=\"ml-2 shrink-0 font-mono text-sm font-medium text-text-primary\">\n {formatTimerDuration(timer.elapsedSeconds)}\n </p>\n </div>\n ))}\n </div>\n )}\n </GlassCard>\n </div>\n\n {/* Start timer modal */}\n <Modal\n open={startOpen}\n onClose={() => setStartOpen(false)}\n title=\"Start a timer\"\n description=\"Track the task you're working on right now.\"\n footer={\n <>\n <button\n onClick={() => setStartOpen(false)}\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={handleStart}\n disabled={starting}\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 {starting ? \"Starting...\" : \"Start Timer\"}\n </button>\n </>\n }\n >\n {startError && (\n <div className=\"mb-4 rounded-lg border border-status-error-bg/40 bg-status-error-bg p-3 text-sm text-status-error-text\">\n Failed to start the timer. Please try again.\n </div>\n )}\n <div className=\"space-y-4\">\n <div>\n <label className=\"block text-sm font-medium text-text-primary\">\n What are you working on?\n </label>\n <input\n type=\"text\"\n placeholder=\"Description\"\n value={description}\n onChange={(e) => setDescription(e.target.value)}\n className=\"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 />\n </div>\n <div>\n <label className=\"block text-sm font-medium text-text-primary\">\n Project\n </label>\n <select\n value={projectId}\n onChange={(e) => setProjectId(e.target.value)}\n className=\"mt-1 w-full rounded-md border border-border-subtle bg-bg-surface px-3 py-2 text-sm text-text-primary\"\n >\n <option value=\"\">No Project</option>\n {projects.map((project) => (\n <option key={project.id} value={project.id}>\n {project.name}\n </option>\n ))}\n </select>\n </div>\n <label className=\"flex items-center gap-2 text-sm text-text-primary\">\n <input\n type=\"checkbox\"\n checked={billable}\n onChange={(e) => setBillable(e.target.checked)}\n className=\"h-4 w-4 rounded border-border-subtle\"\n />\n Billable\n </label>\n </div>\n </Modal>\n\n {/* Reset / discard confirm modal */}\n <Modal\n open={Boolean(resetTarget)}\n onClose={() => setResetTarget(null)}\n title=\"Discard running timer?\"\n description=\"This stops the timer and deletes the in-progress entry. It cannot be undone.\"\n footer={\n <>\n <button\n onClick={() => setResetTarget(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={handleReset}\n disabled={stopping || deleting}\n className=\"rounded-md bg-action-danger-bg px-4 py-2 text-sm font-medium text-action-primary-text hover:bg-action-danger-bg/90 disabled:opacity-50\"\n >\n {stopping || deleting ? \"Discarding...\" : \"Discard\"}\n </button>\n </>\n }\n >\n <p className=\"text-sm text-text-secondary\">\n The timer for{\" \"}\n <span className=\"font-medium text-text-primary\">\n {resetTarget?.timeEntry.project?.name ??\n resetTarget?.timeEntry.description ??\n \"this session\"}\n </span>{\" \"}\n will be discarded.\n </p>\n </Modal>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"TimeApprovalPage.d.ts","sourceRoot":"","sources":["../../../../src/timecampus/pages/time-approval/TimeApprovalPage.tsx"],"names":[],"mappings":"AAuDA,wBAAgB,gBAAgB,gCA2iB/B"}
1
+ {"version":3,"file":"TimeApprovalPage.d.ts","sourceRoot":"","sources":["../../../../src/timecampus/pages/time-approval/TimeApprovalPage.tsx"],"names":[],"mappings":"AAwDA,wBAAgB,gBAAgB,gCA8iB/B"}
@@ -1 +1 @@
1
- {"version":3,"file":"TimeApprovalPage.js","names":[],"sources":["../../../../src/timecampus/pages/time-approval/TimeApprovalPage.tsx"],"sourcesContent":["import { useState } from \"react\";\nimport {\n CheckCircle,\n XCircle,\n X,\n Clock,\n MessageSquare,\n MessagesSquare,\n Undo2,\n} from \"lucide-react\";\nimport { ContextConversationWidget } from \"@burdenoff/fe-libs/shared\";\nimport { useRegisterPageConversationContext } from \"@burdenoff/fe-libs/shared/providers/shell\";\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 { ChartCard, DonutChart, ProgressMeter } from \"../../components\";\nimport { getInitials, formatHours, formatDate, cn } from \"../../utils/helpers\";\nimport {\n useListApprovalRequestsQuery,\n useGetApprovalStatsQuery,\n useApproveRequestMutation,\n useRejectRequestMutation,\n useRequestRevisionMutation,\n useWithdrawRequestMutation,\n type ApprovalRequestFieldsFragment,\n} from \"@/generated/wspace-operations\";\nimport { ApprovalStatus } from \"@/generated/wspace-types\";\n\nconst PAGE_SIZE = 25;\n\n// Rendered as a child of the review Modal so the registration hook only runs\n// while the modal is mounted — it scopes the shared page-conversation widget to\n// the selected approval request, then releases the context on close/unmount.\nfunction ApprovalReviewThread({\n request,\n}: {\n request: ApprovalRequestFieldsFragment;\n}) {\n useRegisterPageConversationContext({\n entityType: \"TIMECAMPUS_APPROVAL_REQUEST\",\n entityId: request.id,\n contextTitle: `Approval · ${request.period ?? request.id}`,\n });\n\n return <ContextConversationWidget widgetKey=\"approval-review\" />;\n}\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\nexport function TimeApprovalPage() {\n const { toasts, addToast, dismissToast } = useToast();\n\n const {\n data: requestsData,\n loading: requestsLoading,\n error: requestsError,\n refetch,\n fetchMore,\n } = useListApprovalRequestsQuery({\n variables: { pagination: { limit: PAGE_SIZE, offset: 0 } },\n notifyOnNetworkStatusChange: true,\n });\n const {\n data: statsData,\n loading: statsLoading,\n error: statsError,\n refetch: refetchStats,\n } = useGetApprovalStatsQuery();\n\n const [approveRequest] = useApproveRequestMutation();\n const [rejectRequest, { loading: rejecting }] = useRejectRequestMutation();\n const [requestRevision, { loading: requestingRevision }] =\n useRequestRevisionMutation();\n const [withdrawRequest] = useWithdrawRequestMutation();\n\n const requests = requestsData?.approvalRequests?.items ?? [];\n const total = requestsData?.approvalRequests?.total ?? 0;\n const hasMore = requestsData?.approvalRequests?.hasMore ?? false;\n const stats = statsData?.approvalStats;\n\n const approvalMix = [\n { name: \"Pending\", value: stats?.pendingCount ?? 0 },\n { name: \"Approved\", value: stats?.approvedCount ?? 0 },\n { name: \"Rejected\", value: stats?.rejectedCount ?? 0 },\n ];\n\n const [loadingMore, setLoadingMore] = useState(false);\n const [busyId, setBusyId] = useState<string | null>(null);\n\n // Reject reason modal\n const [rejectTarget, setRejectTarget] =\n useState<ApprovalRequestFieldsFragment | null>(null);\n const [rejectReason, setRejectReason] = useState(\"\");\n\n // Request revision modal\n const [revisionTarget, setRevisionTarget] =\n useState<ApprovalRequestFieldsFragment | null>(null);\n const [revisionNotes, setRevisionNotes] = useState(\"\");\n\n // Withdraw confirm modal\n const [withdrawTarget, setWithdrawTarget] =\n useState<ApprovalRequestFieldsFragment | null>(null);\n\n // Review / discussion thread modal\n const [reviewTarget, setReviewTarget] =\n useState<ApprovalRequestFieldsFragment | null>(null);\n\n const refreshAll = async () => {\n await Promise.all([refetch(), refetchStats()]);\n };\n\n const handleApprove = async (id: string) => {\n setBusyId(id);\n try {\n await approveRequest({ variables: { id } });\n addToast({ title: \"Timesheet approved\", variant: \"success\" });\n await refreshAll();\n } catch {\n addToast({ title: \"Failed to approve timesheet\", variant: \"error\" });\n } finally {\n setBusyId(null);\n }\n };\n\n const handleReject = async () => {\n if (!rejectTarget || !rejectReason.trim()) return;\n try {\n await rejectRequest({\n variables: { id: rejectTarget.id, reason: rejectReason.trim() },\n });\n addToast({ title: \"Timesheet rejected\", variant: \"success\" });\n setRejectTarget(null);\n setRejectReason(\"\");\n await refreshAll();\n } catch {\n addToast({ title: \"Failed to reject timesheet\", variant: \"error\" });\n }\n };\n\n const handleRequestRevision = async () => {\n if (!revisionTarget || !revisionNotes.trim()) return;\n try {\n await requestRevision({\n variables: { id: revisionTarget.id, notes: revisionNotes.trim() },\n });\n addToast({ title: \"Revision requested\", variant: \"success\" });\n setRevisionTarget(null);\n setRevisionNotes(\"\");\n await refreshAll();\n } catch {\n addToast({ title: \"Failed to request revision\", variant: \"error\" });\n }\n };\n\n const handleWithdraw = async () => {\n if (!withdrawTarget) return;\n try {\n await withdrawRequest({ variables: { id: withdrawTarget.id } });\n addToast({ title: \"Request withdrawn\", variant: \"success\" });\n setWithdrawTarget(null);\n await refreshAll();\n } catch {\n addToast({ title: \"Failed to withdraw request\", variant: \"error\" });\n }\n };\n\n const handleLoadMore = async () => {\n setLoadingMore(true);\n try {\n await fetchMore({\n variables: {\n pagination: { limit: PAGE_SIZE, offset: requests.length },\n },\n updateQuery: (prev, { fetchMoreResult }) => {\n if (!fetchMoreResult) return prev;\n return {\n ...prev,\n approvalRequests: {\n ...fetchMoreResult.approvalRequests,\n items: [\n ...prev.approvalRequests.items,\n ...fetchMoreResult.approvalRequests.items,\n ],\n },\n };\n },\n });\n } finally {\n setLoadingMore(false);\n }\n };\n\n if (requestsLoading && statsLoading && requests.length === 0) {\n return (\n <PageLayout\n title=\"Time Approval\"\n description=\"Review and approve team timesheets\"\n >\n <LoadingSkeleton variant=\"list\" count={4} />\n </PageLayout>\n );\n }\n\n if (requestsError && statsError) {\n return (\n <PageLayout\n title=\"Time Approval\"\n description=\"Review and approve team timesheets\"\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 approval data. Please try again later.\n </div>\n </PageLayout>\n );\n }\n\n return (\n <PageLayout\n title=\"Time Approval\"\n description=\"Review and approve team timesheets\"\n purpose=\"The reviewer's queue for signing off team timesheets before they're billed or paid — approve, reject or request changes in one place so payroll and invoices go out accurate and on time.\"\n >\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4\">\n <StatCard\n featured\n title=\"Pending Approvals\"\n value={String(stats?.pendingCount ?? 0)}\n icon={<Clock className=\"h-5 w-5\" />}\n iconBgColor=\"bg-status-warning-bg text-status-warning-text\"\n />\n <StatCard\n title=\"Approved This Week\"\n value={String(stats?.approvedCount ?? 0)}\n icon={<CheckCircle className=\"h-5 w-5\" />}\n iconBgColor=\"bg-status-success-bg text-status-success-text\"\n />\n <StatCard\n title=\"Rejected\"\n value={String(stats?.rejectedCount ?? 0)}\n icon={<XCircle className=\"h-5 w-5\" />}\n iconBgColor=\"bg-status-error-bg text-status-error-text\"\n />\n <StatCard\n title=\"Avg. Review Time\"\n value={\n stats?.averageReviewTime\n ? `${Math.round(stats.averageReviewTime)}h`\n : \"N/A\"\n }\n description=\"Target: 24h\"\n />\n </div>\n\n <div className=\"grid grid-cols-1 gap-6 lg:grid-cols-3\">\n <div className=\"space-y-4 lg:col-span-2\">\n <div className=\"flex items-center justify-between gap-3\">\n <h2 className=\"text-sm font-semibold text-text-primary\">\n Pending Requests\n </h2>\n <p className=\"text-xs text-text-secondary\">\n Showing {requests.length} of {total}\n </p>\n </div>\n\n {requests.length === 0 && !requestsLoading ? (\n <EmptyState\n illustration=\"empty-notifications\"\n title=\"No approval requests found\"\n description=\"You're all caught up — submitted timesheets waiting on your review will appear here.\"\n />\n ) : (\n <>\n <div className=\"space-y-3\">\n {requests.map((request: ApprovalRequestFieldsFragment) => {\n const isPending = request.status === ApprovalStatus.Pending;\n const rowBusy = busyId === request.id;\n const selected = reviewTarget?.id === request.id;\n const billablePct =\n request.totalHours > 0\n ? Math.round(\n (request.billableHours / request.totalHours) * 100,\n )\n : 0;\n return (\n <GlassCard\n key={request.id}\n treatment=\"plain\"\n glow={selected}\n className={cn(\n \"p-4\",\n selected && \"border-action-primary-bg/50\",\n )}\n >\n <div className=\"flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between\">\n <div className=\"flex min-w-0 items-center gap-3\">\n <div className=\"flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-action-primary-bg/10 text-sm font-medium text-primary\">\n {getInitials(request.userId)}\n </div>\n <div className=\"min-w-0\">\n <p className=\"truncate text-sm font-medium text-text-primary\">\n {request.userId}\n </p>\n <p className=\"truncate text-xs text-text-secondary\">\n {request.period} &middot; Submitted{\" \"}\n {formatDate(request.submittedAt)}\n </p>\n </div>\n </div>\n\n <div className=\"flex flex-col gap-3 sm:flex-row sm:items-center sm:gap-5\">\n <div className=\"flex items-center gap-6\">\n <div>\n <p className=\"text-[11px] font-medium uppercase tracking-wide text-text-secondary\">\n Total submitted\n </p>\n <p className=\"text-lg font-semibold tabular-nums text-text-primary\">\n {formatHours(request.totalHours)}\n </p>\n </div>\n <div>\n <p className=\"text-[11px] font-medium uppercase tracking-wide text-text-secondary\">\n Billable\n </p>\n <p className=\"text-lg font-semibold tabular-nums text-text-primary\">\n {formatHours(request.billableHours)}\n </p>\n </div>\n </div>\n\n <div className=\"flex flex-wrap items-center gap-2\">\n <StatusBadge\n label={request.status.toLowerCase()}\n status={request.status.toLowerCase()}\n />\n {isPending && (\n <>\n <button\n onClick={() => handleApprove(request.id)}\n disabled={rowBusy}\n className=\"inline-flex items-center gap-1.5 rounded-md bg-status-success-bg px-3 py-1.5 text-sm font-medium text-status-success-text hover:bg-status-success-bg/80 disabled:opacity-50\"\n >\n <CheckCircle className=\"h-4 w-4\" />\n Approve\n </button>\n <button\n onClick={() => {\n setRejectTarget(request);\n setRejectReason(\"\");\n }}\n disabled={rowBusy}\n className=\"inline-flex items-center gap-1.5 rounded-md bg-status-error-bg px-3 py-1.5 text-sm font-medium text-status-error-text hover:bg-status-error-bg/80 disabled:opacity-50\"\n >\n <XCircle className=\"h-4 w-4\" />\n Reject\n </button>\n <button\n onClick={() => {\n setRevisionTarget(request);\n setRevisionNotes(\"\");\n }}\n disabled={rowBusy}\n title=\"Request revision\"\n className=\"rounded-md border border-border-seam bg-bg-surface p-1.5 text-status-warning-text hover:bg-status-warning-bg disabled:opacity-50\"\n >\n <MessageSquare className=\"h-4 w-4\" />\n </button>\n <button\n onClick={() => setWithdrawTarget(request)}\n disabled={rowBusy}\n title=\"Withdraw request\"\n className=\"rounded-md border border-border-seam bg-bg-surface p-1.5 text-text-secondary hover:bg-bg-sunken disabled:opacity-50\"\n >\n <Undo2 className=\"h-4 w-4\" />\n </button>\n </>\n )}\n <button\n onClick={() =>\n setReviewTarget(selected ? null : request)\n }\n title=\"Review / Discussion\"\n className={cn(\n \"rounded-md border p-1.5\",\n selected\n ? \"border-action-primary-bg bg-action-primary-bg/10 text-primary\"\n : \"border-border-seam bg-bg-surface text-text-secondary hover:bg-bg-sunken\",\n )}\n >\n <MessagesSquare className=\"h-4 w-4\" />\n </button>\n </div>\n </div>\n </div>\n\n {request.totalHours > 0 && (\n <div className=\"mt-4\">\n <ProgressMeter\n value={billablePct}\n max={100}\n tone=\"success\"\n size=\"sm\"\n showValue={false}\n label={`Billable share · ${billablePct}%`}\n />\n </div>\n )}\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 </div>\n\n <div className=\"lg:col-span-1\">\n <div className=\"space-y-4 lg:sticky lg:top-4\">\n <ChartCard\n title=\"Approval mix\"\n description=\"Requests by current status\"\n >\n <DonutChart\n data={approvalMix}\n centerLabel=\"Total\"\n centerValue={stats?.totalRequests ?? 0}\n height={220}\n emptyLabel=\"No requests to summarise yet.\"\n ariaLabel=\"Approval requests by status\"\n />\n </ChartCard>\n\n <div className=\"space-y-2\">\n <div className=\"flex items-center justify-between px-1\">\n <h3 className=\"text-sm font-medium text-text-primary\">\n Review discussion\n </h3>\n {reviewTarget && (\n <button\n onClick={() => setReviewTarget(null)}\n title=\"Close thread\"\n className=\"inline-flex items-center gap-1 rounded-md px-2 py-1 text-xs text-text-secondary hover:bg-bg-sunken\"\n >\n <X className=\"h-3.5 w-3.5\" />\n Clear\n </button>\n )}\n </div>\n {reviewTarget ? (\n <>\n <p className=\"px-1 text-xs text-text-secondary\">\n Discussing {reviewTarget.userId} &middot;{\" \"}\n {reviewTarget.period}\n </p>\n <ApprovalReviewThread request={reviewTarget} />\n </>\n ) : (\n <GlassCard\n treatment=\"insight\"\n className=\"border-border-seam p-6\"\n >\n <div className=\"flex flex-col items-center justify-center gap-2 text-center\">\n <MessagesSquare className=\"h-6 w-6 text-text-secondary\" />\n <p className=\"text-sm font-medium text-text-primary\">\n No timesheet selected\n </p>\n <p className=\"text-xs text-text-secondary\">\n Open a request's discussion icon to review it with the\n submitter.\n </p>\n </div>\n </GlassCard>\n )}\n </div>\n </div>\n </div>\n </div>\n\n <Modal\n open={rejectTarget !== null}\n onClose={() => setRejectTarget(null)}\n title=\"Reject timesheet\"\n description={\n rejectTarget\n ? `Rejecting ${rejectTarget.userId}'s ${rejectTarget.period} timesheet.`\n : undefined\n }\n size=\"sm\"\n footer={\n <>\n <button\n onClick={() => setRejectTarget(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={handleReject}\n disabled={rejecting || !rejectReason.trim()}\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 {rejecting ? \"Rejecting...\" : \"Reject\"}\n </button>\n </>\n }\n >\n <label className=\"block text-sm font-medium text-text-primary\">\n Rejection reason\n </label>\n <textarea\n rows={3}\n placeholder=\"Explain why this timesheet is being rejected\"\n value={rejectReason}\n onChange={(e) => setRejectReason(e.target.value)}\n className={inputClass}\n />\n </Modal>\n\n <Modal\n open={revisionTarget !== null}\n onClose={() => setRevisionTarget(null)}\n title=\"Request revision\"\n description={\n revisionTarget\n ? `Ask ${revisionTarget.userId} to revise their ${revisionTarget.period} timesheet.`\n : undefined\n }\n size=\"sm\"\n footer={\n <>\n <button\n onClick={() => setRevisionTarget(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={handleRequestRevision}\n disabled={requestingRevision || !revisionNotes.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 {requestingRevision ? \"Sending...\" : \"Request Revision\"}\n </button>\n </>\n }\n >\n <label className=\"block text-sm font-medium text-text-primary\">\n What needs changing?\n </label>\n <textarea\n rows={3}\n placeholder=\"Describe the changes needed before this can be approved\"\n value={revisionNotes}\n onChange={(e) => setRevisionNotes(e.target.value)}\n className={inputClass}\n />\n </Modal>\n\n <Modal\n open={withdrawTarget !== null}\n onClose={() => setWithdrawTarget(null)}\n title=\"Withdraw request?\"\n description={\n withdrawTarget\n ? `${withdrawTarget.userId}'s ${withdrawTarget.period} timesheet will be pulled from the review queue.`\n : undefined\n }\n size=\"sm\"\n footer={\n <>\n <button\n onClick={() => setWithdrawTarget(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={handleWithdraw}\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\"\n >\n Withdraw\n </button>\n </>\n }\n >\n <p className=\"text-sm text-text-secondary\">\n Withdrawing removes the request from the approval queue. It can be\n resubmitted later.\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":"TimeApprovalPage.js","names":[],"sources":["../../../../src/timecampus/pages/time-approval/TimeApprovalPage.tsx"],"sourcesContent":["import { useState } from \"react\";\nimport {\n CheckCircle,\n XCircle,\n X,\n Clock,\n MessageSquare,\n MessagesSquare,\n Undo2,\n} from \"lucide-react\";\nimport { ContextConversationWidget } from \"@burdenoff/fe-libs/shared\";\nimport { useRegisterPageConversationContext } from \"@burdenoff/fe-libs/shared/providers/shell\";\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 { ChartCard, DonutChart, ProgressMeter } from \"../../components\";\nimport { getInitials, formatHours, formatDate, cn } from \"../../utils/helpers\";\nimport {\n useListApprovalRequestsQuery,\n useGetApprovalStatsQuery,\n useApproveRequestMutation,\n useRejectRequestMutation,\n useRequestRevisionMutation,\n useWithdrawRequestMutation,\n type ApprovalRequestFieldsFragment,\n type ListApprovalRequestsQuery,\n} from \"@/generated/wspace-operations\";\nimport { ApprovalStatus } from \"@/generated/wspace-types\";\n\nconst PAGE_SIZE = 25;\n\n// Rendered as a child of the review Modal so the registration hook only runs\n// while the modal is mounted — it scopes the shared page-conversation widget to\n// the selected approval request, then releases the context on close/unmount.\nfunction ApprovalReviewThread({\n request,\n}: {\n request: ApprovalRequestFieldsFragment;\n}) {\n useRegisterPageConversationContext({\n entityType: \"TIMECAMPUS_APPROVAL_REQUEST\",\n entityId: request.id,\n contextTitle: `Approval · ${request.period ?? request.id}`,\n });\n\n return <ContextConversationWidget widgetKey=\"approval-review\" />;\n}\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\nexport function TimeApprovalPage() {\n const { toasts, addToast, dismissToast } = useToast();\n\n const {\n data: requestsData,\n loading: requestsLoading,\n error: requestsError,\n refetch,\n fetchMore,\n } = useListApprovalRequestsQuery({\n variables: { pagination: { limit: PAGE_SIZE, offset: 0 } },\n notifyOnNetworkStatusChange: true,\n });\n const {\n data: statsData,\n loading: statsLoading,\n error: statsError,\n refetch: refetchStats,\n } = useGetApprovalStatsQuery();\n\n const [approveRequest] = useApproveRequestMutation();\n const [rejectRequest, { loading: rejecting }] = useRejectRequestMutation();\n const [requestRevision, { loading: requestingRevision }] =\n useRequestRevisionMutation();\n const [withdrawRequest] = useWithdrawRequestMutation();\n\n const requests = requestsData?.approvalRequests?.items ?? [];\n const total = requestsData?.approvalRequests?.total ?? 0;\n const hasMore = requestsData?.approvalRequests?.hasMore ?? false;\n const stats = statsData?.approvalStats;\n\n const approvalMix = [\n { name: \"Pending\", value: stats?.pendingCount ?? 0 },\n { name: \"Approved\", value: stats?.approvedCount ?? 0 },\n { name: \"Rejected\", value: stats?.rejectedCount ?? 0 },\n ];\n\n const [loadingMore, setLoadingMore] = useState(false);\n const [busyId, setBusyId] = useState<string | null>(null);\n\n // Reject reason modal\n const [rejectTarget, setRejectTarget] =\n useState<ApprovalRequestFieldsFragment | null>(null);\n const [rejectReason, setRejectReason] = useState(\"\");\n\n // Request revision modal\n const [revisionTarget, setRevisionTarget] =\n useState<ApprovalRequestFieldsFragment | null>(null);\n const [revisionNotes, setRevisionNotes] = useState(\"\");\n\n // Withdraw confirm modal\n const [withdrawTarget, setWithdrawTarget] =\n useState<ApprovalRequestFieldsFragment | null>(null);\n\n // Review / discussion thread modal\n const [reviewTarget, setReviewTarget] =\n useState<ApprovalRequestFieldsFragment | null>(null);\n\n const refreshAll = async () => {\n await Promise.all([refetch(), refetchStats()]);\n };\n\n const handleApprove = async (id: string) => {\n setBusyId(id);\n try {\n await approveRequest({ variables: { id } });\n addToast({ title: \"Timesheet approved\", variant: \"success\" });\n await refreshAll();\n } catch {\n addToast({ title: \"Failed to approve timesheet\", variant: \"error\" });\n } finally {\n setBusyId(null);\n }\n };\n\n const handleReject = async () => {\n if (!rejectTarget || !rejectReason.trim()) return;\n try {\n await rejectRequest({\n variables: { id: rejectTarget.id, reason: rejectReason.trim() },\n });\n addToast({ title: \"Timesheet rejected\", variant: \"success\" });\n setRejectTarget(null);\n setRejectReason(\"\");\n await refreshAll();\n } catch {\n addToast({ title: \"Failed to reject timesheet\", variant: \"error\" });\n }\n };\n\n const handleRequestRevision = async () => {\n if (!revisionTarget || !revisionNotes.trim()) return;\n try {\n await requestRevision({\n variables: { id: revisionTarget.id, notes: revisionNotes.trim() },\n });\n addToast({ title: \"Revision requested\", variant: \"success\" });\n setRevisionTarget(null);\n setRevisionNotes(\"\");\n await refreshAll();\n } catch {\n addToast({ title: \"Failed to request revision\", variant: \"error\" });\n }\n };\n\n const handleWithdraw = async () => {\n if (!withdrawTarget) return;\n try {\n await withdrawRequest({ variables: { id: withdrawTarget.id } });\n addToast({ title: \"Request withdrawn\", variant: \"success\" });\n setWithdrawTarget(null);\n await refreshAll();\n } catch {\n addToast({ title: \"Failed to withdraw request\", variant: \"error\" });\n }\n };\n\n const handleLoadMore = async () => {\n setLoadingMore(true);\n try {\n await fetchMore({\n variables: {\n pagination: { limit: PAGE_SIZE, offset: requests.length },\n },\n updateQuery: (\n prev: ListApprovalRequestsQuery,\n { fetchMoreResult }: { fetchMoreResult?: ListApprovalRequestsQuery },\n ) => {\n if (!fetchMoreResult) return prev;\n return {\n ...prev,\n approvalRequests: {\n ...fetchMoreResult.approvalRequests,\n items: [\n ...prev.approvalRequests.items,\n ...fetchMoreResult.approvalRequests.items,\n ],\n },\n };\n },\n });\n } finally {\n setLoadingMore(false);\n }\n };\n\n if (requestsLoading && statsLoading && requests.length === 0) {\n return (\n <PageLayout\n title=\"Time Approval\"\n description=\"Review and approve team timesheets\"\n >\n <LoadingSkeleton variant=\"list\" count={4} />\n </PageLayout>\n );\n }\n\n if (requestsError && statsError) {\n return (\n <PageLayout\n title=\"Time Approval\"\n description=\"Review and approve team timesheets\"\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 approval data. Please try again later.\n </div>\n </PageLayout>\n );\n }\n\n return (\n <PageLayout\n title=\"Time Approval\"\n description=\"Review and approve team timesheets\"\n purpose=\"The reviewer's queue for signing off team timesheets before they're billed or paid — approve, reject or request changes in one place so payroll and invoices go out accurate and on time.\"\n >\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4\">\n <StatCard\n featured\n title=\"Pending Approvals\"\n value={String(stats?.pendingCount ?? 0)}\n icon={<Clock className=\"h-5 w-5\" />}\n iconBgColor=\"bg-status-warning-bg text-status-warning-text\"\n />\n <StatCard\n title=\"Approved This Week\"\n value={String(stats?.approvedCount ?? 0)}\n icon={<CheckCircle className=\"h-5 w-5\" />}\n iconBgColor=\"bg-status-success-bg text-status-success-text\"\n />\n <StatCard\n title=\"Rejected\"\n value={String(stats?.rejectedCount ?? 0)}\n icon={<XCircle className=\"h-5 w-5\" />}\n iconBgColor=\"bg-status-error-bg text-status-error-text\"\n />\n <StatCard\n title=\"Avg. Review Time\"\n value={\n stats?.averageReviewTime\n ? `${Math.round(stats.averageReviewTime)}h`\n : \"N/A\"\n }\n description=\"Target: 24h\"\n />\n </div>\n\n <div className=\"grid grid-cols-1 gap-6 lg:grid-cols-3\">\n <div className=\"space-y-4 lg:col-span-2\">\n <div className=\"flex items-center justify-between gap-3\">\n <h2 className=\"text-sm font-semibold text-text-primary\">\n Pending Requests\n </h2>\n <p className=\"text-xs text-text-secondary\">\n Showing {requests.length} of {total}\n </p>\n </div>\n\n {requests.length === 0 && !requestsLoading ? (\n <EmptyState\n illustration=\"empty-notifications\"\n title=\"No approval requests found\"\n description=\"You're all caught up — submitted timesheets waiting on your review will appear here.\"\n />\n ) : (\n <>\n <div className=\"space-y-3\">\n {requests.map((request: ApprovalRequestFieldsFragment) => {\n const isPending = request.status === ApprovalStatus.Pending;\n const rowBusy = busyId === request.id;\n const selected = reviewTarget?.id === request.id;\n const billablePct =\n request.totalHours > 0\n ? Math.round(\n (request.billableHours / request.totalHours) * 100,\n )\n : 0;\n return (\n <GlassCard\n key={request.id}\n treatment=\"plain\"\n glow={selected}\n className={cn(\n \"p-4\",\n selected && \"border-action-primary-bg/50\",\n )}\n >\n <div className=\"flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between\">\n <div className=\"flex min-w-0 items-center gap-3\">\n <div className=\"flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-action-primary-bg/10 text-sm font-medium text-primary\">\n {getInitials(request.userId)}\n </div>\n <div className=\"min-w-0\">\n <p className=\"truncate text-sm font-medium text-text-primary\">\n {request.userId}\n </p>\n <p className=\"truncate text-xs text-text-secondary\">\n {request.period} &middot; Submitted{\" \"}\n {formatDate(request.submittedAt)}\n </p>\n </div>\n </div>\n\n <div className=\"flex flex-col gap-3 sm:flex-row sm:items-center sm:gap-5\">\n <div className=\"flex items-center gap-6\">\n <div>\n <p className=\"text-[11px] font-medium uppercase tracking-wide text-text-secondary\">\n Total submitted\n </p>\n <p className=\"text-lg font-semibold tabular-nums text-text-primary\">\n {formatHours(request.totalHours)}\n </p>\n </div>\n <div>\n <p className=\"text-[11px] font-medium uppercase tracking-wide text-text-secondary\">\n Billable\n </p>\n <p className=\"text-lg font-semibold tabular-nums text-text-primary\">\n {formatHours(request.billableHours)}\n </p>\n </div>\n </div>\n\n <div className=\"flex flex-wrap items-center gap-2\">\n <StatusBadge\n label={request.status.toLowerCase()}\n status={request.status.toLowerCase()}\n />\n {isPending && (\n <>\n <button\n onClick={() => handleApprove(request.id)}\n disabled={rowBusy}\n className=\"inline-flex items-center gap-1.5 rounded-md bg-status-success-bg px-3 py-1.5 text-sm font-medium text-status-success-text hover:bg-status-success-bg/80 disabled:opacity-50\"\n >\n <CheckCircle className=\"h-4 w-4\" />\n Approve\n </button>\n <button\n onClick={() => {\n setRejectTarget(request);\n setRejectReason(\"\");\n }}\n disabled={rowBusy}\n className=\"inline-flex items-center gap-1.5 rounded-md bg-status-error-bg px-3 py-1.5 text-sm font-medium text-status-error-text hover:bg-status-error-bg/80 disabled:opacity-50\"\n >\n <XCircle className=\"h-4 w-4\" />\n Reject\n </button>\n <button\n onClick={() => {\n setRevisionTarget(request);\n setRevisionNotes(\"\");\n }}\n disabled={rowBusy}\n title=\"Request revision\"\n className=\"rounded-md border border-border-seam bg-bg-surface p-1.5 text-status-warning-text hover:bg-status-warning-bg disabled:opacity-50\"\n >\n <MessageSquare className=\"h-4 w-4\" />\n </button>\n <button\n onClick={() => setWithdrawTarget(request)}\n disabled={rowBusy}\n title=\"Withdraw request\"\n className=\"rounded-md border border-border-seam bg-bg-surface p-1.5 text-text-secondary hover:bg-bg-sunken disabled:opacity-50\"\n >\n <Undo2 className=\"h-4 w-4\" />\n </button>\n </>\n )}\n <button\n onClick={() =>\n setReviewTarget(selected ? null : request)\n }\n title=\"Review / Discussion\"\n className={cn(\n \"rounded-md border p-1.5\",\n selected\n ? \"border-action-primary-bg bg-action-primary-bg/10 text-primary\"\n : \"border-border-seam bg-bg-surface text-text-secondary hover:bg-bg-sunken\",\n )}\n >\n <MessagesSquare className=\"h-4 w-4\" />\n </button>\n </div>\n </div>\n </div>\n\n {request.totalHours > 0 && (\n <div className=\"mt-4\">\n <ProgressMeter\n value={billablePct}\n max={100}\n tone=\"success\"\n size=\"sm\"\n showValue={false}\n label={`Billable share · ${billablePct}%`}\n />\n </div>\n )}\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 </div>\n\n <div className=\"lg:col-span-1\">\n <div className=\"space-y-4 lg:sticky lg:top-4\">\n <ChartCard\n title=\"Approval mix\"\n description=\"Requests by current status\"\n >\n <DonutChart\n data={approvalMix}\n centerLabel=\"Total\"\n centerValue={stats?.totalRequests ?? 0}\n height={220}\n emptyLabel=\"No requests to summarise yet.\"\n ariaLabel=\"Approval requests by status\"\n />\n </ChartCard>\n\n <div className=\"space-y-2\">\n <div className=\"flex items-center justify-between px-1\">\n <h3 className=\"text-sm font-medium text-text-primary\">\n Review discussion\n </h3>\n {reviewTarget && (\n <button\n onClick={() => setReviewTarget(null)}\n title=\"Close thread\"\n className=\"inline-flex items-center gap-1 rounded-md px-2 py-1 text-xs text-text-secondary hover:bg-bg-sunken\"\n >\n <X className=\"h-3.5 w-3.5\" />\n Clear\n </button>\n )}\n </div>\n {reviewTarget ? (\n <>\n <p className=\"px-1 text-xs text-text-secondary\">\n Discussing {reviewTarget.userId} &middot;{\" \"}\n {reviewTarget.period}\n </p>\n <ApprovalReviewThread request={reviewTarget} />\n </>\n ) : (\n <GlassCard\n treatment=\"insight\"\n className=\"border-border-seam p-6\"\n >\n <div className=\"flex flex-col items-center justify-center gap-2 text-center\">\n <MessagesSquare className=\"h-6 w-6 text-text-secondary\" />\n <p className=\"text-sm font-medium text-text-primary\">\n No timesheet selected\n </p>\n <p className=\"text-xs text-text-secondary\">\n Open a request's discussion icon to review it with the\n submitter.\n </p>\n </div>\n </GlassCard>\n )}\n </div>\n </div>\n </div>\n </div>\n\n <Modal\n open={rejectTarget !== null}\n onClose={() => setRejectTarget(null)}\n title=\"Reject timesheet\"\n description={\n rejectTarget\n ? `Rejecting ${rejectTarget.userId}'s ${rejectTarget.period} timesheet.`\n : undefined\n }\n size=\"sm\"\n footer={\n <>\n <button\n onClick={() => setRejectTarget(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={handleReject}\n disabled={rejecting || !rejectReason.trim()}\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 {rejecting ? \"Rejecting...\" : \"Reject\"}\n </button>\n </>\n }\n >\n <label className=\"block text-sm font-medium text-text-primary\">\n Rejection reason\n </label>\n <textarea\n rows={3}\n placeholder=\"Explain why this timesheet is being rejected\"\n value={rejectReason}\n onChange={(e) => setRejectReason(e.target.value)}\n className={inputClass}\n />\n </Modal>\n\n <Modal\n open={revisionTarget !== null}\n onClose={() => setRevisionTarget(null)}\n title=\"Request revision\"\n description={\n revisionTarget\n ? `Ask ${revisionTarget.userId} to revise their ${revisionTarget.period} timesheet.`\n : undefined\n }\n size=\"sm\"\n footer={\n <>\n <button\n onClick={() => setRevisionTarget(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={handleRequestRevision}\n disabled={requestingRevision || !revisionNotes.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 {requestingRevision ? \"Sending...\" : \"Request Revision\"}\n </button>\n </>\n }\n >\n <label className=\"block text-sm font-medium text-text-primary\">\n What needs changing?\n </label>\n <textarea\n rows={3}\n placeholder=\"Describe the changes needed before this can be approved\"\n value={revisionNotes}\n onChange={(e) => setRevisionNotes(e.target.value)}\n className={inputClass}\n />\n </Modal>\n\n <Modal\n open={withdrawTarget !== null}\n onClose={() => setWithdrawTarget(null)}\n title=\"Withdraw request?\"\n description={\n withdrawTarget\n ? `${withdrawTarget.userId}'s ${withdrawTarget.period} timesheet will be pulled from the review queue.`\n : undefined\n }\n size=\"sm\"\n footer={\n <>\n <button\n onClick={() => setWithdrawTarget(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={handleWithdraw}\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\"\n >\n Withdraw\n </button>\n </>\n }\n >\n <p className=\"text-sm text-text-secondary\">\n Withdrawing removes the request from the approval queue. It can be\n resubmitted later.\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":"TimelinePage.js","names":[],"sources":["../../../../src/timecampus/pages/timeline/TimelinePage.tsx"],"sourcesContent":["import { useMemo, useState } from \"react\";\nimport {\n DndContext,\n DragOverlay,\n KeyboardSensor,\n PointerSensor,\n useDraggable,\n useDroppable,\n useSensor,\n useSensors,\n type DragEndEvent,\n type DragStartEvent,\n} from \"@dnd-kit/core\";\nimport {\n Activity,\n Brain,\n Calendar,\n Coffee,\n Github,\n GripVertical,\n MessageSquare,\n Users,\n Zap,\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 { formatDuration, formatTime } from \"../../utils/helpers\";\nimport { useTimeCampus } from \"../../providers/TimeCampusProvider\";\nimport {\n useGetTimeCampusTimelineQuery,\n useRecategorizeTimeCampusCaptureEventMutation,\n type TimelineSegmentFieldsFragment,\n} from \"@/generated/wspace-operations\";\n\ntype Category = \"MEETING\" | \"DEEP_WORK\" | \"COMMUNICATION\" | \"BREAK\" | \"OTHER\";\n\nconst CATEGORY_META: Record<\n Category,\n { label: string; icon: typeof Brain; dot: string; chip: string }\n> = {\n DEEP_WORK: {\n label: \"Deep work\",\n icon: Brain,\n dot: \"bg-status-success-bg\",\n chip: \"bg-status-success-bg text-status-success-text\",\n },\n MEETING: {\n label: \"Meeting\",\n icon: Users,\n dot: \"bg-status-info-bg\",\n chip: \"bg-status-info-bg text-status-info-text\",\n },\n COMMUNICATION: {\n label: \"Communication\",\n icon: MessageSquare,\n dot: \"bg-status-warning-bg\",\n chip: \"bg-status-warning-bg text-status-warning-text\",\n },\n BREAK: {\n label: \"Break\",\n icon: Coffee,\n dot: \"bg-bg-sunken\",\n chip: \"bg-bg-sunken text-text-secondary\",\n },\n OTHER: {\n label: \"Other\",\n icon: Activity,\n dot: \"bg-bg-sunken\",\n chip: \"bg-bg-sunken text-text-secondary\",\n },\n};\n\nconst SOURCE_ICON: Record<string, typeof Brain> = {\n TIME_ENTRY: Zap,\n MEETING: Users,\n POMODORO: Brain,\n EMAIL: MessageSquare,\n CAPTURE: Github,\n};\n\nconst CATEGORY_ORDER: Category[] = [\n \"DEEP_WORK\",\n \"MEETING\",\n \"COMMUNICATION\",\n \"BREAK\",\n \"OTHER\",\n];\n\n/** Droppable target: a category chip in the \"Where the time went\" summary. */\nfunction DroppableCategoryChip({\n category,\n label,\n dotClassName,\n totalSeconds,\n}: {\n category: Category;\n label: string;\n dotClassName: string;\n totalSeconds: number;\n}) {\n const { isOver, setNodeRef } = useDroppable({ id: `category:${category}` });\n return (\n <div\n ref={setNodeRef}\n className={`flex items-center gap-2 rounded-lg border px-3 py-2 transition-colors ${\n isOver\n ? \"border-action-primary-bg bg-action-primary-bg/10\"\n : \"border-border-seam bg-bg-surface\"\n }`}\n >\n <span\n className={`inline-block h-2.5 w-2.5 rounded-full ${dotClassName}`}\n aria-hidden\n />\n <span className=\"text-sm font-medium text-text-primary\">{label}</span>\n <span className=\"text-sm tabular-nums text-text-secondary\">\n {formatDuration(totalSeconds)}\n </span>\n {isOver && (\n <span className=\"text-xs font-medium text-primary\">Drop here</span>\n )}\n </div>\n );\n}\n\n/** Draggable row for a single timeline segment, with the existing dropdown kept as an accessible fallback. */\nfunction DraggableSegmentRow({\n segment,\n category,\n recategorizing,\n onRecategorize,\n}: {\n segment: TimelineSegmentFieldsFragment;\n category: Category;\n recategorizing: boolean;\n onRecategorize: (\n segment: TimelineSegmentFieldsFragment,\n category: Category,\n ) => void;\n}) {\n const meta = CATEGORY_META[category] ?? CATEGORY_META.OTHER;\n const SourceIcon = SOURCE_ICON[segment.source] ?? Activity;\n const { attributes, listeners, setNodeRef, isDragging } = useDraggable({\n id: segment.id,\n data: { segment },\n disabled: !segment.isRecategorizable,\n });\n\n return (\n <li\n ref={setNodeRef}\n className={`flex items-start gap-3 p-4 transition-colors hover:bg-bg-sunken/50 ${\n isDragging ? \"opacity-40\" : \"\"\n }`}\n >\n <div className=\"flex w-16 shrink-0 flex-col items-end pt-0.5 text-xs tabular-nums text-text-secondary\">\n <span>{formatTime(segment.startTime)}</span>\n <span className=\"text-text-tertiary\">\n {formatDuration(segment.durationSeconds)}\n </span>\n </div>\n {segment.isRecategorizable && (\n <button\n type=\"button\"\n aria-label={`Drag to recategorize ${segment.title || meta.label}`}\n className=\"mt-1.5 shrink-0 cursor-grab touch-none text-text-tertiary hover:text-text-secondary active:cursor-grabbing\"\n {...attributes}\n {...listeners}\n >\n <GripVertical className=\"h-4 w-4\" />\n </button>\n )}\n <div\n className={`mt-0.5 flex h-8 w-8 shrink-0 items-center justify-center rounded-lg ${meta.chip}`}\n >\n <SourceIcon className=\"h-4 w-4\" />\n </div>\n <div className=\"min-w-0 flex-1\">\n <p className=\"truncate text-sm font-medium text-text-primary\">\n {segment.title || meta.label}\n </p>\n <div className=\"mt-1 flex flex-wrap items-center gap-2\">\n <span\n className={`inline-flex rounded-full px-2 py-0.5 text-xs font-medium ${meta.chip}`}\n >\n {meta.label}\n </span>\n {segment.app && (\n <span className=\"text-xs text-text-secondary\">{segment.app}</span>\n )}\n {segment.isRecategorizable && (\n <span className=\"text-xs text-text-tertiary\">via capture</span>\n )}\n </div>\n </div>\n {segment.isRecategorizable && (\n <select\n aria-label=\"Recategorize activity\"\n disabled={recategorizing}\n value={category}\n onChange={(e) => onRecategorize(segment, e.target.value as Category)}\n className=\"shrink-0 rounded-md border border-border-seam bg-bg-surface px-2 py-1 text-xs text-text-secondary focus:outline-none focus:ring-2 focus:ring-action-primary-bg/40\"\n >\n {CATEGORY_ORDER.map((c) => (\n <option key={c} value={c}>\n {CATEGORY_META[c].label}\n </option>\n ))}\n </select>\n )}\n </li>\n );\n}\n\nfunction toLocalDateInput(date: Date): string {\n const offset = date.getTimezoneOffset();\n const local = new Date(date.getTime() - offset * 60 * 1000);\n return local.toISOString().slice(0, 10);\n}\n\nfunction dayBounds(dateStr: string): { from: string; to: string } {\n const start = new Date(`${dateStr}T00:00:00`);\n const end = new Date(`${dateStr}T23:59:59.999`);\n return { from: start.toISOString(), to: end.toISOString() };\n}\n\nexport function TimelinePage() {\n const { workspaceId = \"\" } = useTimeCampus();\n const [dateStr, setDateStr] = useState(() => toLocalDateInput(new Date()));\n const [draggingSegment, setDraggingSegment] =\n useState<TimelineSegmentFieldsFragment | null>(null);\n\n const { from, to } = useMemo(() => dayBounds(dateStr), [dateStr]);\n\n const { data, loading, error, refetch } = useGetTimeCampusTimelineQuery({\n variables: { from, to },\n skip: !workspaceId,\n fetchPolicy: \"cache-and-network\",\n });\n\n const [recategorize, { loading: recategorizing }] =\n useRecategorizeTimeCampusCaptureEventMutation();\n\n const sensors = useSensors(\n useSensor(PointerSensor, { activationConstraint: { distance: 6 } }),\n useSensor(KeyboardSensor),\n );\n\n const timeline = data?.timeCampusTimeline;\n const segments = timeline?.segments ?? [];\n const summary = timeline?.summary;\n\n const totalsByCategory = useMemo(() => {\n const map = new Map<\n string,\n { totalSeconds: number; segmentCount: number }\n >();\n for (const t of summary?.byCategory ?? []) {\n map.set(t.category, {\n totalSeconds: t.totalSeconds,\n segmentCount: t.segmentCount,\n });\n }\n return map;\n }, [summary]);\n\n const handleRecategorize = async (\n segment: TimelineSegmentFieldsFragment,\n category: Category,\n ) => {\n if (!segment.isRecategorizable || segment.category === category) return;\n await recategorize({\n variables: { id: segment.sourceId, category },\n });\n await refetch();\n };\n\n const handleDragStart = (event: DragStartEvent) => {\n const segment = event.active.data.current?.segment as\n TimelineSegmentFieldsFragment | undefined;\n setDraggingSegment(segment ?? null);\n };\n\n const handleDragEnd = (event: DragEndEvent) => {\n const segment = event.active.data.current?.segment as\n TimelineSegmentFieldsFragment | undefined;\n setDraggingSegment(null);\n const overId = event.over?.id;\n if (!segment || typeof overId !== \"string\") return;\n const category = overId.replace(/^category:/, \"\") as Category;\n if (!CATEGORY_ORDER.includes(category)) return;\n void handleRecategorize(segment, category);\n };\n\n const focusPct =\n summary && summary.focusRatio != null\n ? Math.round(summary.focusRatio * 100)\n : 0;\n\n return (\n <PageLayout\n title=\"Timeline\"\n description=\"Your day, assembled automatically\"\n purpose=\"See where your time actually went — meetings, deep work, communication and breaks stitched into one honest, chronological view from everything TimeCampus captures, so you can spot the patterns worth changing.\"\n actions={\n <label className=\"flex items-center gap-2 text-sm text-text-secondary\">\n <Calendar className=\"h-4 w-4\" />\n <input\n type=\"date\"\n value={dateStr}\n max={toLocalDateInput(new Date())}\n onChange={(e) => setDateStr(e.target.value)}\n className=\"rounded-md border border-border-seam bg-bg-surface px-3 py-1.5 text-sm text-text-primary focus:outline-none focus:ring-2 focus:ring-action-primary-bg/40\"\n />\n </label>\n }\n >\n {loading && !timeline ? (\n <div className=\"space-y-6\">\n <LoadingSkeleton variant=\"card\" count={4} />\n <LoadingSkeleton variant=\"list\" count={6} />\n </div>\n ) : error ? (\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 your timeline. Please try again later.\n </div>\n ) : (\n <DndContext\n sensors={sensors}\n onDragStart={handleDragStart}\n onDragEnd={handleDragEnd}\n >\n <div className=\"space-y-6\">\n {/* Summary */}\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4\">\n <StatCard\n featured\n title=\"Tracked time\"\n value={formatDuration(summary?.totalTrackedSeconds ?? 0)}\n icon={<Activity className=\"h-5 w-5\" />}\n description=\"Total activity across the day\"\n />\n <StatCard\n title=\"Focus ratio\"\n value={`${focusPct}%`}\n icon={<Brain className=\"h-5 w-5\" />}\n description=\"Share of time in deep work\"\n />\n <StatCard\n title=\"Context switches\"\n value={summary?.contextSwitches ?? 0}\n icon={<Zap className=\"h-5 w-5\" />}\n description=\"Category changes through the day\"\n />\n <StatCard\n title=\"Longest focus block\"\n value={formatDuration(summary?.longestFocusBlockSeconds ?? 0)}\n icon={<Brain className=\"h-5 w-5\" />}\n description=\"Your deepest uninterrupted stretch\"\n />\n </div>\n\n {/* Category breakdown — also the drop targets for drag-to-recategorize */}\n {segments.length > 0 && (\n <GlassCard treatment=\"glass\" className=\"p-4 sm:p-6\">\n <h2 className=\"mb-1 text-sm font-semibold text-text-primary\">\n Where the time went\n </h2>\n <p className=\"mb-4 text-xs text-text-secondary\">\n Drag a segment from the timeline below onto a category to\n recategorize it — or use its dropdown.\n </p>\n <div className=\"flex flex-wrap gap-4\">\n {CATEGORY_ORDER.filter((c) => totalsByCategory.has(c)).map(\n (category) => {\n const meta = CATEGORY_META[category];\n const total = totalsByCategory.get(category)!;\n return (\n <DroppableCategoryChip\n key={category}\n category={category}\n label={meta.label}\n dotClassName={meta.dot}\n totalSeconds={total.totalSeconds}\n />\n );\n },\n )}\n </div>\n </GlassCard>\n )}\n\n {/* Chronological timeline */}\n {segments.length === 0 ? (\n <EmptyState\n illustration=\"empty-generic\"\n title=\"No activity captured for this day\"\n description=\"As you track time, hold meetings, run focus sessions, and connect shared integrations, your day assembles here automatically — categorized and in order.\"\n />\n ) : (\n <GlassCard treatment=\"glass\" glow className=\"overflow-hidden\">\n <ol className=\"divide-y divide-border-seam\">\n {segments.map((segment: TimelineSegmentFieldsFragment) => (\n <DraggableSegmentRow\n key={segment.id}\n segment={segment}\n category={(segment.category as Category) ?? \"OTHER\"}\n recategorizing={recategorizing}\n onRecategorize={handleRecategorize}\n />\n ))}\n </ol>\n </GlassCard>\n )}\n </div>\n\n <DragOverlay>\n {draggingSegment && (\n <div className=\"flex items-center gap-2 rounded-lg border border-border-seam bg-bg-surface px-3 py-2 shadow-pop\">\n <GripVertical className=\"h-4 w-4 text-text-tertiary\" />\n <span className=\"text-sm font-medium text-text-primary\">\n {draggingSegment.title ||\n CATEGORY_META[\n (draggingSegment.category as Category) ?? \"OTHER\"\n ].label}\n </span>\n </div>\n )}\n </DragOverlay>\n </DndContext>\n )}\n </PageLayout>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"TimelinePage.js","names":[],"sources":["../../../../src/timecampus/pages/timeline/TimelinePage.tsx"],"sourcesContent":["import { useMemo, useState } from \"react\";\nimport {\n DndContext,\n DragOverlay,\n KeyboardSensor,\n PointerSensor,\n useDraggable,\n useDroppable,\n useSensor,\n useSensors,\n type DragEndEvent,\n type DragStartEvent,\n} from \"@dnd-kit/core\";\nimport {\n Activity,\n Brain,\n Calendar,\n Coffee,\n Github,\n GripVertical,\n MessageSquare,\n Users,\n Zap,\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 { formatDuration, formatTime } from \"../../utils/helpers\";\nimport { useTimeCampus } from \"../../providers/TimeCampusProvider\";\nimport {\n useGetTimeCampusTimelineQuery,\n useRecategorizeTimeCampusCaptureEventMutation,\n type TimelineSegmentFieldsFragment,\n} from \"@/generated/wspace-operations\";\n\ntype Category = \"MEETING\" | \"DEEP_WORK\" | \"COMMUNICATION\" | \"BREAK\" | \"OTHER\";\n\nconst CATEGORY_META: Record<\n Category,\n { label: string; icon: typeof Brain; dot: string; chip: string }\n> = {\n DEEP_WORK: {\n label: \"Deep work\",\n icon: Brain,\n dot: \"bg-status-success-bg\",\n chip: \"bg-status-success-bg text-status-success-text\",\n },\n MEETING: {\n label: \"Meeting\",\n icon: Users,\n dot: \"bg-status-info-bg\",\n chip: \"bg-status-info-bg text-status-info-text\",\n },\n COMMUNICATION: {\n label: \"Communication\",\n icon: MessageSquare,\n dot: \"bg-status-warning-bg\",\n chip: \"bg-status-warning-bg text-status-warning-text\",\n },\n BREAK: {\n label: \"Break\",\n icon: Coffee,\n dot: \"bg-bg-sunken\",\n chip: \"bg-bg-sunken text-text-secondary\",\n },\n OTHER: {\n label: \"Other\",\n icon: Activity,\n dot: \"bg-bg-sunken\",\n chip: \"bg-bg-sunken text-text-secondary\",\n },\n};\n\nconst SOURCE_ICON: Record<string, typeof Brain> = {\n TIME_ENTRY: Zap,\n MEETING: Users,\n POMODORO: Brain,\n EMAIL: MessageSquare,\n CAPTURE: Github,\n};\n\nconst CATEGORY_ORDER: Category[] = [\n \"DEEP_WORK\",\n \"MEETING\",\n \"COMMUNICATION\",\n \"BREAK\",\n \"OTHER\",\n];\n\n/** Droppable target: a category chip in the \"Where the time went\" summary. */\nfunction DroppableCategoryChip({\n category,\n label,\n dotClassName,\n totalSeconds,\n}: {\n category: Category;\n label: string;\n dotClassName: string;\n totalSeconds: number;\n}) {\n const { isOver, setNodeRef } = useDroppable({ id: `category:${category}` });\n return (\n <div\n ref={setNodeRef}\n className={`flex items-center gap-2 rounded-lg border px-3 py-2 transition-colors ${\n isOver\n ? \"border-action-primary-bg bg-action-primary-bg/10\"\n : \"border-border-seam bg-bg-surface\"\n }`}\n >\n <span\n className={`inline-block h-2.5 w-2.5 rounded-full ${dotClassName}`}\n aria-hidden\n />\n <span className=\"text-sm font-medium text-text-primary\">{label}</span>\n <span className=\"text-sm tabular-nums text-text-secondary\">\n {formatDuration(totalSeconds)}\n </span>\n {isOver && (\n <span className=\"text-xs font-medium text-primary\">Drop here</span>\n )}\n </div>\n );\n}\n\n/** Draggable row for a single timeline segment, with the existing dropdown kept as an accessible fallback. */\nfunction DraggableSegmentRow({\n segment,\n category,\n recategorizing,\n onRecategorize,\n}: {\n segment: TimelineSegmentFieldsFragment;\n category: Category;\n recategorizing: boolean;\n onRecategorize: (\n segment: TimelineSegmentFieldsFragment,\n category: Category,\n ) => void;\n}) {\n const meta = CATEGORY_META[category] ?? CATEGORY_META.OTHER;\n const SourceIcon = SOURCE_ICON[segment.source] ?? Activity;\n const { attributes, listeners, setNodeRef, isDragging } = useDraggable({\n id: segment.id,\n data: { segment },\n disabled: !segment.isRecategorizable,\n });\n\n return (\n <li\n ref={setNodeRef}\n className={`flex items-start gap-3 p-4 transition-colors hover:bg-bg-sunken/50 ${\n isDragging ? \"opacity-40\" : \"\"\n }`}\n >\n <div className=\"flex w-16 shrink-0 flex-col items-end pt-0.5 text-xs tabular-nums text-text-secondary\">\n <span>{formatTime(segment.startTime)}</span>\n <span className=\"text-text-tertiary\">\n {formatDuration(segment.durationSeconds)}\n </span>\n </div>\n {segment.isRecategorizable && (\n <button\n type=\"button\"\n aria-label={`Drag to recategorize ${segment.title || meta.label}`}\n className=\"mt-1.5 shrink-0 cursor-grab touch-none text-text-tertiary hover:text-text-secondary active:cursor-grabbing\"\n {...attributes}\n {...listeners}\n >\n <GripVertical className=\"h-4 w-4\" />\n </button>\n )}\n <div\n className={`mt-0.5 flex h-8 w-8 shrink-0 items-center justify-center rounded-lg ${meta.chip}`}\n >\n <SourceIcon className=\"h-4 w-4\" />\n </div>\n <div className=\"min-w-0 flex-1\">\n <p className=\"truncate text-sm font-medium text-text-primary\">\n {segment.title || meta.label}\n </p>\n <div className=\"mt-1 flex flex-wrap items-center gap-2\">\n <span\n className={`inline-flex rounded-full px-2 py-0.5 text-xs font-medium ${meta.chip}`}\n >\n {meta.label}\n </span>\n {segment.app && (\n <span className=\"text-xs text-text-secondary\">{segment.app}</span>\n )}\n {segment.isRecategorizable && (\n <span className=\"text-xs text-text-tertiary\">via capture</span>\n )}\n </div>\n </div>\n {segment.isRecategorizable && (\n <select\n aria-label=\"Recategorize activity\"\n disabled={recategorizing}\n value={category}\n onChange={(e) => onRecategorize(segment, e.target.value as Category)}\n className=\"shrink-0 rounded-md border border-border-seam bg-bg-surface px-2 py-1 text-xs text-text-secondary focus:outline-none focus:ring-2 focus:ring-action-primary-bg/40\"\n >\n {CATEGORY_ORDER.map((c) => (\n <option key={c} value={c}>\n {CATEGORY_META[c].label}\n </option>\n ))}\n </select>\n )}\n </li>\n );\n}\n\nfunction toLocalDateInput(date: Date): string {\n const offset = date.getTimezoneOffset();\n const local = new Date(date.getTime() - offset * 60 * 1000);\n return local.toISOString().slice(0, 10);\n}\n\nfunction dayBounds(dateStr: string): { from: string; to: string } {\n const start = new Date(`${dateStr}T00:00:00`);\n const end = new Date(`${dateStr}T23:59:59.999`);\n return { from: start.toISOString(), to: end.toISOString() };\n}\n\nexport function TimelinePage() {\n const { workspaceId = \"\" } = useTimeCampus();\n const [dateStr, setDateStr] = useState(() => toLocalDateInput(new Date()));\n const [draggingSegment, setDraggingSegment] =\n useState<TimelineSegmentFieldsFragment | null>(null);\n\n const { from, to } = useMemo(() => dayBounds(dateStr), [dateStr]);\n\n const { data, loading, error, refetch } = useGetTimeCampusTimelineQuery({\n variables: { from, to },\n skip: !workspaceId,\n fetchPolicy: \"cache-and-network\",\n });\n\n const [recategorize, { loading: recategorizing }] =\n useRecategorizeTimeCampusCaptureEventMutation();\n\n const sensors = useSensors(\n useSensor(PointerSensor, { activationConstraint: { distance: 6 } }),\n useSensor(KeyboardSensor),\n );\n\n const timeline = data?.timeCampusTimeline;\n const segments = timeline?.segments ?? [];\n const summary = timeline?.summary;\n\n const totalsByCategory = useMemo(() => {\n const map = new Map<\n string,\n { totalSeconds: number; segmentCount: number }\n >();\n for (const t of summary?.byCategory ?? []) {\n map.set(t.category, {\n totalSeconds: t.totalSeconds,\n segmentCount: t.segmentCount,\n });\n }\n return map;\n }, [summary]);\n\n const handleRecategorize = async (\n segment: TimelineSegmentFieldsFragment,\n category: Category,\n ) => {\n if (!segment.isRecategorizable || segment.category === category) return;\n await recategorize({\n variables: { id: segment.sourceId, category },\n });\n await refetch();\n };\n\n const handleDragStart = (event: DragStartEvent) => {\n const segment = event.active.data.current?.segment as\n TimelineSegmentFieldsFragment | undefined;\n setDraggingSegment(segment ?? null);\n };\n\n const handleDragEnd = (event: DragEndEvent) => {\n const segment = event.active.data.current?.segment as\n TimelineSegmentFieldsFragment | undefined;\n setDraggingSegment(null);\n const overId = event.over?.id;\n if (!segment || typeof overId !== \"string\") return;\n const category = overId.replace(/^category:/, \"\") as Category;\n if (!CATEGORY_ORDER.includes(category)) return;\n void handleRecategorize(segment, category);\n };\n\n const focusPct =\n summary && summary.focusRatio != null\n ? Math.round(summary.focusRatio * 100)\n : 0;\n\n return (\n <PageLayout\n title=\"Timeline\"\n description=\"Your day, assembled automatically\"\n purpose=\"See where your time actually went — meetings, deep work, communication and breaks stitched into one honest, chronological view from everything TimeCampus captures, so you can spot the patterns worth changing.\"\n actions={\n <label className=\"flex items-center gap-2 text-sm text-text-secondary\">\n <Calendar className=\"h-4 w-4\" />\n <input\n type=\"date\"\n value={dateStr}\n max={toLocalDateInput(new Date())}\n onChange={(e) => setDateStr(e.target.value)}\n className=\"rounded-md border border-border-seam bg-bg-surface px-3 py-1.5 text-sm text-text-primary focus:outline-none focus:ring-2 focus:ring-action-primary-bg/40\"\n />\n </label>\n }\n >\n {loading && !timeline ? (\n <div className=\"space-y-6\">\n <LoadingSkeleton variant=\"card\" count={4} />\n <LoadingSkeleton variant=\"list\" count={6} />\n </div>\n ) : error ? (\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 your timeline. Please try again later.\n </div>\n ) : (\n <DndContext\n sensors={sensors}\n onDragStart={handleDragStart}\n onDragEnd={handleDragEnd}\n >\n <div className=\"space-y-6\">\n {/* Summary */}\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4\">\n <StatCard\n featured\n title=\"Tracked time\"\n value={formatDuration(summary?.totalTrackedSeconds ?? 0)}\n icon={<Activity className=\"h-5 w-5\" />}\n description=\"Total activity across the day\"\n />\n <StatCard\n title=\"Focus ratio\"\n value={`${focusPct}%`}\n icon={<Brain className=\"h-5 w-5\" />}\n description=\"Share of time in deep work\"\n />\n <StatCard\n title=\"Context switches\"\n value={summary?.contextSwitches ?? 0}\n icon={<Zap className=\"h-5 w-5\" />}\n description=\"Category changes through the day\"\n />\n <StatCard\n title=\"Longest focus block\"\n value={formatDuration(summary?.longestFocusBlockSeconds ?? 0)}\n icon={<Brain className=\"h-5 w-5\" />}\n description=\"Your deepest uninterrupted stretch\"\n />\n </div>\n\n {/* Category breakdown — also the drop targets for drag-to-recategorize */}\n {segments.length > 0 && (\n <GlassCard treatment=\"glass\" className=\"p-4 sm:p-6\">\n <h2 className=\"mb-1 text-sm font-semibold text-text-primary\">\n Where the time went\n </h2>\n <p className=\"mb-4 text-xs text-text-secondary\">\n Drag a segment from the timeline below onto a category to\n recategorize it — or use its dropdown.\n </p>\n <div className=\"flex flex-wrap gap-4\">\n {CATEGORY_ORDER.filter((c) => totalsByCategory.has(c)).map(\n (category) => {\n const meta = CATEGORY_META[category];\n const total = totalsByCategory.get(category)!;\n return (\n <DroppableCategoryChip\n key={category}\n category={category}\n label={meta.label}\n dotClassName={meta.dot}\n totalSeconds={total.totalSeconds}\n />\n );\n },\n )}\n </div>\n </GlassCard>\n )}\n\n {/* Chronological timeline */}\n {segments.length === 0 ? (\n <EmptyState\n illustration=\"empty-generic\"\n title=\"No activity captured for this day\"\n description=\"As you track time, hold meetings, run focus sessions, and connect shared integrations, your day assembles here automatically — categorized and in order.\"\n />\n ) : (\n <GlassCard treatment=\"glass\" glow className=\"overflow-hidden\">\n <ol className=\"divide-y divide-border-seam\">\n {segments.map((segment: TimelineSegmentFieldsFragment) => (\n <DraggableSegmentRow\n key={segment.id}\n segment={segment}\n category={(segment.category as Category) ?? \"OTHER\"}\n recategorizing={recategorizing}\n onRecategorize={handleRecategorize}\n />\n ))}\n </ol>\n </GlassCard>\n )}\n </div>\n\n <DragOverlay>\n {draggingSegment && (\n <div className=\"flex items-center gap-2 rounded-lg border border-border-seam bg-bg-surface px-3 py-2 shadow-pop\">\n <GripVertical className=\"h-4 w-4 text-text-tertiary\" />\n <span className=\"text-sm font-medium text-text-primary\">\n {draggingSegment.title ||\n CATEGORY_META[\n (draggingSegment.category as Category) ?? \"OTHER\"\n ].label}\n </span>\n </div>\n )}\n </DragOverlay>\n </DndContext>\n )}\n </PageLayout>\n );\n}\n"],"mappings":"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"}
@@ -49,7 +49,7 @@ var re = {
49
49
  NEUTRAL: 3,
50
50
  POSITIVE: 4,
51
51
  VERY_POSITIVE: 5
52
- }, ae = new Set(["HIGH", "VERY_HIGH"]), oe = new Set(["LOW", "VERY_LOW"]), se = (e) => `${e.toFixed(1)}h`, ce = {
52
+ }, ae = /* @__PURE__ */ new Set(["HIGH", "VERY_HIGH"]), oe = /* @__PURE__ */ new Set(["LOW", "VERY_LOW"]), se = (e) => `${e.toFixed(1)}h`, ce = {
53
53
  [u.Positive]: "success",
54
54
  [u.Negative]: "error",
55
55
  [u.Warning]: "warning",
@@ -1 +1 @@
1
- {"version":3,"file":"WorkPatternsPage.js","names":[],"sources":["../../../../src/timecampus/pages/work-patterns/WorkPatternsPage.tsx"],"sourcesContent":["import {\n TrendingUp,\n Clock,\n Zap,\n Brain,\n AlertTriangle,\n ShieldCheck,\n Lightbulb,\n ArrowRight,\n} from \"lucide-react\";\nimport { GlassCard } from \"@burdenoff/fe-libs/ui\";\nimport {\n PageLayout,\n StatCard,\n ChartCard,\n Heatmap,\n TrendChart,\n StatusBadge,\n EmptyState,\n LoadingSkeleton,\n type HeatmapValue,\n} from \"../../components\";\nimport { useTimeCampus } from \"../../providers/TimeCampusProvider\";\nimport {\n useListWorkPatternsQuery,\n useGetWorkPatternSummaryQuery,\n type WorkPatternFieldsFragment,\n} from \"@/generated/wspace-operations\";\nimport {\n WorkPatternInsightType,\n type WorkPatternInsight,\n} from \"@/generated/wspace-types\";\n\nconst DAY_LABELS = [\"Mon\", \"Tue\", \"Wed\", \"Thu\", \"Fri\", \"Sat\", \"Sun\"] as const;\n\n/** JS `getDay()` is 0=Sun..6=Sat; remap so Monday is index 0. */\nfunction dayOfWeekIndex(d: Date): number {\n return (d.getDay() + 6) % 7;\n}\n\n/** Monday (local midnight) of the week containing `d`. */\nfunction weekStart(d: Date): Date {\n const s = new Date(d);\n s.setDate(d.getDate() - dayOfWeekIndex(d));\n s.setHours(0, 0, 0, 0);\n return s;\n}\n\nfunction shortDate(iso: string): string {\n const d = new Date(iso);\n if (Number.isNaN(d.getTime())) return \"\";\n return d.toLocaleDateString(undefined, { month: \"short\", day: \"numeric\" });\n}\n\n// Self-reported ordinals mapped onto a shared 1 (worst) – 5 (best) scale so\n// energy and mood can be trended together. Only real, fetched enum values map;\n// anything else stays null (a gap in the line, never a fabricated point).\nconst ENERGY_SCORE: Record<string, number> = {\n VERY_LOW: 1,\n LOW: 2,\n MEDIUM: 3,\n HIGH: 4,\n VERY_HIGH: 5,\n};\nconst MOOD_SCORE: Record<string, number> = {\n VERY_NEGATIVE: 1,\n NEGATIVE: 2,\n NEUTRAL: 3,\n POSITIVE: 4,\n VERY_POSITIVE: 5,\n};\n\nconst HIGH_STRESS = new Set([\"HIGH\", \"VERY_HIGH\"]);\nconst LOW_ENERGY = new Set([\"LOW\", \"VERY_LOW\"]);\n\nconst formatHoursValue = (v: number) => `${v.toFixed(1)}h`;\n\ntype BurnoutTone = \"danger\" | \"warning\" | \"success\";\n\nconst INSIGHT_VARIANT: Record<\n WorkPatternInsightType,\n \"success\" | \"error\" | \"warning\" | \"info\" | \"muted\"\n> = {\n [WorkPatternInsightType.Positive]: \"success\",\n [WorkPatternInsightType.Negative]: \"error\",\n [WorkPatternInsightType.Warning]: \"warning\",\n [WorkPatternInsightType.Recommendation]: \"info\",\n [WorkPatternInsightType.Neutral]: \"muted\",\n};\n\nconst INSIGHT_LABEL: Record<WorkPatternInsightType, string> = {\n [WorkPatternInsightType.Positive]: \"Positive\",\n [WorkPatternInsightType.Negative]: \"Negative\",\n [WorkPatternInsightType.Warning]: \"Warning\",\n [WorkPatternInsightType.Recommendation]: \"Tip\",\n [WorkPatternInsightType.Neutral]: \"Neutral\",\n};\n\nexport function WorkPatternsPage() {\n const { workspaceId = \"\" } = useTimeCampus();\n const {\n data: patternsData,\n loading: patternsLoading,\n error: patternsError,\n } = useListWorkPatternsQuery({\n variables: { pagination: { limit: 30 } },\n skip: !workspaceId,\n });\n\n const {\n data: summaryData,\n loading: summaryLoading,\n error: summaryError,\n } = useGetWorkPatternSummaryQuery({\n skip: !workspaceId,\n });\n\n const loading = patternsLoading || summaryLoading;\n const error = patternsError || summaryError;\n const summary = summaryData?.workPatternSummary;\n const patterns = patternsData?.workPatterns ?? [];\n const insights = summary?.insights ?? [];\n\n // Chronological order for time-based visuals; the list itself is newest-first.\n const chronological = [...patterns].sort((a, b) => {\n const ta = new Date(a.date).getTime();\n const tb = new Date(b.date).getTime();\n return (Number.isNaN(ta) ? 0 : ta) - (Number.isNaN(tb) ? 0 : tb);\n });\n\n // Derive stats from the latest pattern entry\n const latestPattern = patterns[0];\n const productivityScore =\n summary?.averageProductivity != null\n ? `${Math.round(summary.averageProductivity)}%`\n : \"--\";\n const focusHours =\n latestPattern?.focusHours != null\n ? `${latestPattern.focusHours.toFixed(1)}h`\n : \"--\";\n const totalHours = latestPattern\n ? (\n latestPattern.focusHours +\n latestPattern.meetingHours +\n latestPattern.breakHours\n ).toFixed(0)\n : \"8\";\n const deepWorkSessions =\n latestPattern?.deepWorkSessions != null\n ? String(latestPattern.deepWorkSessions)\n : \"--\";\n const mostProductiveDay = summary?.mostProductiveDay ?? \"--\";\n\n // ---- Heatmap: focus (productive) hours by weekday × week -----------------\n const weekKeys: number[] = [];\n for (const p of chronological) {\n const d = new Date(p.date);\n if (Number.isNaN(d.getTime())) continue;\n const key = weekStart(d).getTime();\n if (!weekKeys.includes(key)) weekKeys.push(key);\n }\n const selectedWeeks = weekKeys.slice(-6); // most recent 6 weeks\n const columnLabels = selectedWeeks.map((ts) =>\n shortDate(new Date(ts).toISOString()),\n );\n const heatmapData: HeatmapValue[][] = DAY_LABELS.map(() =>\n selectedWeeks.map<HeatmapValue>(() => null),\n );\n for (const p of chronological) {\n const d = new Date(p.date);\n if (Number.isNaN(d.getTime())) continue;\n const weekIdx = selectedWeeks.indexOf(weekStart(d).getTime());\n if (weekIdx < 0) continue;\n heatmapData[dayOfWeekIndex(d)][weekIdx] = p.focusHours;\n }\n\n // ---- Energy & mood trend --------------------------------------------------\n const trendData = chronological.map((p) => ({\n date: shortDate(p.date),\n energy:\n p.energyLevel != null ? (ENERGY_SCORE[p.energyLevel] ?? null) : null,\n mood: p.mood != null ? (MOOD_SCORE[p.mood] ?? null) : null,\n }));\n const hasEnergyMood = trendData.some(\n (d) => d.energy != null || d.mood != null,\n );\n\n // ---- Burnout signal (derived from real stress/energy + backend insights) --\n const recent = patterns.slice(0, 7); // most recent logged days\n const windowSize = recent.length;\n const highStressDays = recent.filter(\n (p: WorkPatternFieldsFragment) =>\n p.stressLevel != null && HIGH_STRESS.has(p.stressLevel),\n ).length;\n const lowEnergyDays = recent.filter(\n (p: WorkPatternFieldsFragment) =>\n p.energyLevel != null && LOW_ENERGY.has(p.energyLevel),\n ).length;\n const warningInsight = insights.find(\n (i: WorkPatternInsight) =>\n i.type === WorkPatternInsightType.Warning ||\n i.type === WorkPatternInsightType.Negative,\n );\n const half = Math.ceil(Math.max(windowSize, 1) / 2);\n\n let burnoutTone: BurnoutTone = \"success\";\n if (windowSize > 0 && highStressDays >= half) burnoutTone = \"danger\";\n else if (highStressDays > 0 || lowEnergyDays >= half || warningInsight)\n burnoutTone = \"warning\";\n if (warningInsight?.type === WorkPatternInsightType.Negative)\n burnoutTone = \"danger\";\n\n const stressEvidence: string[] = [];\n if (highStressDays > 0)\n stressEvidence.push(\n `${highStressDays} of your last ${windowSize} logged day${windowSize === 1 ? \"\" : \"s\"} showed high stress`,\n );\n if (lowEnergyDays > 0)\n stressEvidence.push(`${lowEnergyDays} showed low energy`);\n\n const burnoutHeading =\n warningInsight?.title ??\n (burnoutTone === \"danger\"\n ? \"Sustained overload detected\"\n : burnoutTone === \"warning\"\n ? \"Watch your workload\"\n : \"Balanced workload\");\n const burnoutBody =\n warningInsight?.description ??\n (burnoutTone === \"success\"\n ? \"Your recent stress and energy levels look healthy — keep protecting your focus blocks.\"\n : stressEvidence.length > 0\n ? `${stressEvidence.join(\" and \")}. Protect your deep-work time before it erodes.`\n : \"Recent signals suggest an uneven balance — schedule focus time to stay ahead of overload.\");\n const burnoutRecommendation = warningInsight?.recommendation ?? null;\n\n const burnoutAccent =\n burnoutTone === \"danger\"\n ? \"bg-status-error-bg text-status-error-text\"\n : burnoutTone === \"warning\"\n ? \"bg-status-warning-bg text-status-warning-text\"\n : \"bg-status-success-bg text-status-success-text\";\n\n if (loading) {\n return (\n <PageLayout\n title=\"Work Patterns\"\n description=\"Analyze your work habits and productivity patterns\"\n >\n <LoadingSkeleton variant=\"card\" count={4} />\n </PageLayout>\n );\n }\n\n if (error) {\n return (\n <PageLayout\n title=\"Work Patterns\"\n description=\"Analyze your work habits and productivity patterns\"\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 work patterns. Please try again later.\n </div>\n </PageLayout>\n );\n }\n\n return (\n <PageLayout\n title=\"Work Patterns\"\n description=\"Analyze your work habits and productivity patterns\"\n purpose=\"Learn how you actually work across a typical week — when you focus best, where meetings pile up, and which day is your peak — so you can schedule deep work for the hours you're sharpest.\"\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=\"Productivity Score\"\n value={productivityScore}\n icon={<TrendingUp className=\"h-5 w-5\" />}\n trend={\n summary?.averageProductivity != null\n ? { value: 5, label: \"vs last week\" }\n : undefined\n }\n />\n <StatCard\n title=\"Focus Hours\"\n value={focusHours}\n icon={<Brain className=\"h-5 w-5\" />}\n description={`Out of ${totalHours}h total`}\n />\n <StatCard\n title=\"Peak Day\"\n value={mostProductiveDay}\n icon={<Zap className=\"h-5 w-5\" />}\n description=\"Most productive day\"\n />\n <StatCard\n title=\"Deep Work\"\n value={`${deepWorkSessions} sessions`}\n icon={<Clock className=\"h-5 w-5\" />}\n description=\"90+ min uninterrupted\"\n />\n </div>\n\n {/* Burnout signal — honest, derived from recent stress/energy + insights */}\n <GlassCard\n treatment=\"glass\"\n glow={burnoutTone !== \"success\"}\n className=\"p-5 sm:p-6\"\n >\n <div className=\"flex flex-col gap-4 sm:flex-row sm:items-start\">\n <div\n className={`flex h-11 w-11 shrink-0 items-center justify-center rounded-xl ${burnoutAccent}`}\n >\n {burnoutTone === \"success\" ? (\n <ShieldCheck className=\"h-5 w-5\" />\n ) : (\n <AlertTriangle className=\"h-5 w-5\" />\n )}\n </div>\n <div className=\"min-w-0 flex-1\">\n <div className=\"flex flex-wrap items-center gap-2\">\n <h3 className=\"text-base font-semibold text-text-primary\">\n Burnout Signal\n </h3>\n <StatusBadge\n variant={\n burnoutTone === \"danger\"\n ? \"error\"\n : burnoutTone === \"warning\"\n ? \"warning\"\n : \"success\"\n }\n label={burnoutHeading}\n />\n </div>\n <p className=\"mt-1.5 text-sm text-text-secondary\">\n {burnoutBody}\n </p>\n {burnoutRecommendation && (\n <p className=\"mt-2 flex items-start gap-1.5 text-sm text-text-primary\">\n <Lightbulb className=\"mt-0.5 h-4 w-4 shrink-0 text-primary\" />\n <span>{burnoutRecommendation}</span>\n </p>\n )}\n </div>\n </div>\n </GlassCard>\n\n <div className=\"grid grid-cols-1 gap-6 lg:grid-cols-2\">\n <ChartCard\n title=\"Weekly Productive Hours\"\n description=\"Focused hours logged by weekday across recent weeks\"\n >\n <Heatmap\n data={heatmapData}\n rowLabels={[...DAY_LABELS]}\n columnLabels={columnLabels}\n rowsLabel=\"Day\"\n valueFormat={formatHoursValue}\n legendLabels={[\"Fewer\", \"More\"]}\n height={240}\n emptyLabel=\"No work-pattern days logged yet.\"\n ariaLabel=\"Focused hours by weekday and week\"\n />\n </ChartCard>\n\n <ChartCard\n title=\"Energy & Mood Trends\"\n description=\"Self-reported energy and mood, each on a 1 (low) – 5 (high) scale\"\n >\n {hasEnergyMood ? (\n <TrendChart\n data={trendData}\n xKey=\"date\"\n series={[\n { key: \"energy\", label: \"Energy\" },\n { key: \"mood\", label: \"Mood\" },\n ]}\n type=\"area\"\n height={240}\n emptyLabel=\"No energy or mood check-ins yet.\"\n ariaLabel=\"Energy and mood over time\"\n />\n ) : (\n <EmptyState\n title=\"No energy or mood data yet\"\n description=\"Log how you feel on your work-pattern entries to see the trend here.\"\n />\n )}\n </ChartCard>\n </div>\n\n {insights.length > 0 && (\n <GlassCard treatment=\"glass\" className=\"p-5 sm:p-6\">\n <h3 className=\"mb-4 flex items-center gap-2 text-sm font-semibold text-text-primary\">\n <Lightbulb className=\"h-4 w-4 text-primary\" />\n Insights\n </h3>\n <div className=\"space-y-3\">\n {insights.map((insight: WorkPatternInsight, idx: number) => (\n <div\n key={`${insight.type}-${idx}`}\n className=\"rounded-lg border border-border-subtle bg-bg-surface/40 p-4\"\n >\n <div className=\"flex flex-wrap items-center gap-2\">\n <StatusBadge\n variant={INSIGHT_VARIANT[insight.type]}\n label={INSIGHT_LABEL[insight.type]}\n />\n <p className=\"text-sm font-medium text-text-primary\">\n {insight.title}\n </p>\n </div>\n <p className=\"mt-1.5 text-sm text-text-secondary\">\n {insight.description}\n </p>\n {insight.recommendation && (\n <p className=\"mt-2 flex items-start gap-1.5 text-xs text-text-primary\">\n <ArrowRight className=\"mt-0.5 h-3.5 w-3.5 shrink-0 text-primary\" />\n <span>{insight.recommendation}</span>\n </p>\n )}\n </div>\n ))}\n </div>\n </GlassCard>\n )}\n </div>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"WorkPatternsPage.js","names":[],"sources":["../../../../src/timecampus/pages/work-patterns/WorkPatternsPage.tsx"],"sourcesContent":["import {\n TrendingUp,\n Clock,\n Zap,\n Brain,\n AlertTriangle,\n ShieldCheck,\n Lightbulb,\n ArrowRight,\n} from \"lucide-react\";\nimport { GlassCard } from \"@burdenoff/fe-libs/ui\";\nimport {\n PageLayout,\n StatCard,\n ChartCard,\n Heatmap,\n TrendChart,\n StatusBadge,\n EmptyState,\n LoadingSkeleton,\n type HeatmapValue,\n} from \"../../components\";\nimport { useTimeCampus } from \"../../providers/TimeCampusProvider\";\nimport {\n useListWorkPatternsQuery,\n useGetWorkPatternSummaryQuery,\n type WorkPatternFieldsFragment,\n} from \"@/generated/wspace-operations\";\nimport {\n WorkPatternInsightType,\n type WorkPatternInsight,\n} from \"@/generated/wspace-types\";\n\nconst DAY_LABELS = [\"Mon\", \"Tue\", \"Wed\", \"Thu\", \"Fri\", \"Sat\", \"Sun\"] as const;\n\n/** JS `getDay()` is 0=Sun..6=Sat; remap so Monday is index 0. */\nfunction dayOfWeekIndex(d: Date): number {\n return (d.getDay() + 6) % 7;\n}\n\n/** Monday (local midnight) of the week containing `d`. */\nfunction weekStart(d: Date): Date {\n const s = new Date(d);\n s.setDate(d.getDate() - dayOfWeekIndex(d));\n s.setHours(0, 0, 0, 0);\n return s;\n}\n\nfunction shortDate(iso: string): string {\n const d = new Date(iso);\n if (Number.isNaN(d.getTime())) return \"\";\n return d.toLocaleDateString(undefined, { month: \"short\", day: \"numeric\" });\n}\n\n// Self-reported ordinals mapped onto a shared 1 (worst) – 5 (best) scale so\n// energy and mood can be trended together. Only real, fetched enum values map;\n// anything else stays null (a gap in the line, never a fabricated point).\nconst ENERGY_SCORE: Record<string, number> = {\n VERY_LOW: 1,\n LOW: 2,\n MEDIUM: 3,\n HIGH: 4,\n VERY_HIGH: 5,\n};\nconst MOOD_SCORE: Record<string, number> = {\n VERY_NEGATIVE: 1,\n NEGATIVE: 2,\n NEUTRAL: 3,\n POSITIVE: 4,\n VERY_POSITIVE: 5,\n};\n\nconst HIGH_STRESS = new Set([\"HIGH\", \"VERY_HIGH\"]);\nconst LOW_ENERGY = new Set([\"LOW\", \"VERY_LOW\"]);\n\nconst formatHoursValue = (v: number) => `${v.toFixed(1)}h`;\n\ntype BurnoutTone = \"danger\" | \"warning\" | \"success\";\n\nconst INSIGHT_VARIANT: Record<\n WorkPatternInsightType,\n \"success\" | \"error\" | \"warning\" | \"info\" | \"muted\"\n> = {\n [WorkPatternInsightType.Positive]: \"success\",\n [WorkPatternInsightType.Negative]: \"error\",\n [WorkPatternInsightType.Warning]: \"warning\",\n [WorkPatternInsightType.Recommendation]: \"info\",\n [WorkPatternInsightType.Neutral]: \"muted\",\n};\n\nconst INSIGHT_LABEL: Record<WorkPatternInsightType, string> = {\n [WorkPatternInsightType.Positive]: \"Positive\",\n [WorkPatternInsightType.Negative]: \"Negative\",\n [WorkPatternInsightType.Warning]: \"Warning\",\n [WorkPatternInsightType.Recommendation]: \"Tip\",\n [WorkPatternInsightType.Neutral]: \"Neutral\",\n};\n\nexport function WorkPatternsPage() {\n const { workspaceId = \"\" } = useTimeCampus();\n const {\n data: patternsData,\n loading: patternsLoading,\n error: patternsError,\n } = useListWorkPatternsQuery({\n variables: { pagination: { limit: 30 } },\n skip: !workspaceId,\n });\n\n const {\n data: summaryData,\n loading: summaryLoading,\n error: summaryError,\n } = useGetWorkPatternSummaryQuery({\n skip: !workspaceId,\n });\n\n const loading = patternsLoading || summaryLoading;\n const error = patternsError || summaryError;\n const summary = summaryData?.workPatternSummary;\n const patterns = patternsData?.workPatterns ?? [];\n const insights = summary?.insights ?? [];\n\n // Chronological order for time-based visuals; the list itself is newest-first.\n const chronological = [...patterns].sort((a, b) => {\n const ta = new Date(a.date).getTime();\n const tb = new Date(b.date).getTime();\n return (Number.isNaN(ta) ? 0 : ta) - (Number.isNaN(tb) ? 0 : tb);\n });\n\n // Derive stats from the latest pattern entry\n const latestPattern = patterns[0];\n const productivityScore =\n summary?.averageProductivity != null\n ? `${Math.round(summary.averageProductivity)}%`\n : \"--\";\n const focusHours =\n latestPattern?.focusHours != null\n ? `${latestPattern.focusHours.toFixed(1)}h`\n : \"--\";\n const totalHours = latestPattern\n ? (\n latestPattern.focusHours +\n latestPattern.meetingHours +\n latestPattern.breakHours\n ).toFixed(0)\n : \"8\";\n const deepWorkSessions =\n latestPattern?.deepWorkSessions != null\n ? String(latestPattern.deepWorkSessions)\n : \"--\";\n const mostProductiveDay = summary?.mostProductiveDay ?? \"--\";\n\n // ---- Heatmap: focus (productive) hours by weekday × week -----------------\n const weekKeys: number[] = [];\n for (const p of chronological) {\n const d = new Date(p.date);\n if (Number.isNaN(d.getTime())) continue;\n const key = weekStart(d).getTime();\n if (!weekKeys.includes(key)) weekKeys.push(key);\n }\n const selectedWeeks = weekKeys.slice(-6); // most recent 6 weeks\n const columnLabels = selectedWeeks.map((ts) =>\n shortDate(new Date(ts).toISOString()),\n );\n const heatmapData: HeatmapValue[][] = DAY_LABELS.map(() =>\n selectedWeeks.map<HeatmapValue>(() => null),\n );\n for (const p of chronological) {\n const d = new Date(p.date);\n if (Number.isNaN(d.getTime())) continue;\n const weekIdx = selectedWeeks.indexOf(weekStart(d).getTime());\n if (weekIdx < 0) continue;\n heatmapData[dayOfWeekIndex(d)][weekIdx] = p.focusHours;\n }\n\n // ---- Energy & mood trend --------------------------------------------------\n const trendData = chronological.map((p) => ({\n date: shortDate(p.date),\n energy:\n p.energyLevel != null ? (ENERGY_SCORE[p.energyLevel] ?? null) : null,\n mood: p.mood != null ? (MOOD_SCORE[p.mood] ?? null) : null,\n }));\n const hasEnergyMood = trendData.some(\n (d) => d.energy != null || d.mood != null,\n );\n\n // ---- Burnout signal (derived from real stress/energy + backend insights) --\n const recent = patterns.slice(0, 7); // most recent logged days\n const windowSize = recent.length;\n const highStressDays = recent.filter(\n (p: WorkPatternFieldsFragment) =>\n p.stressLevel != null && HIGH_STRESS.has(p.stressLevel),\n ).length;\n const lowEnergyDays = recent.filter(\n (p: WorkPatternFieldsFragment) =>\n p.energyLevel != null && LOW_ENERGY.has(p.energyLevel),\n ).length;\n const warningInsight = insights.find(\n (i: WorkPatternInsight) =>\n i.type === WorkPatternInsightType.Warning ||\n i.type === WorkPatternInsightType.Negative,\n );\n const half = Math.ceil(Math.max(windowSize, 1) / 2);\n\n let burnoutTone: BurnoutTone = \"success\";\n if (windowSize > 0 && highStressDays >= half) burnoutTone = \"danger\";\n else if (highStressDays > 0 || lowEnergyDays >= half || warningInsight)\n burnoutTone = \"warning\";\n if (warningInsight?.type === WorkPatternInsightType.Negative)\n burnoutTone = \"danger\";\n\n const stressEvidence: string[] = [];\n if (highStressDays > 0)\n stressEvidence.push(\n `${highStressDays} of your last ${windowSize} logged day${windowSize === 1 ? \"\" : \"s\"} showed high stress`,\n );\n if (lowEnergyDays > 0)\n stressEvidence.push(`${lowEnergyDays} showed low energy`);\n\n const burnoutHeading =\n warningInsight?.title ??\n (burnoutTone === \"danger\"\n ? \"Sustained overload detected\"\n : burnoutTone === \"warning\"\n ? \"Watch your workload\"\n : \"Balanced workload\");\n const burnoutBody =\n warningInsight?.description ??\n (burnoutTone === \"success\"\n ? \"Your recent stress and energy levels look healthy — keep protecting your focus blocks.\"\n : stressEvidence.length > 0\n ? `${stressEvidence.join(\" and \")}. Protect your deep-work time before it erodes.`\n : \"Recent signals suggest an uneven balance — schedule focus time to stay ahead of overload.\");\n const burnoutRecommendation = warningInsight?.recommendation ?? null;\n\n const burnoutAccent =\n burnoutTone === \"danger\"\n ? \"bg-status-error-bg text-status-error-text\"\n : burnoutTone === \"warning\"\n ? \"bg-status-warning-bg text-status-warning-text\"\n : \"bg-status-success-bg text-status-success-text\";\n\n if (loading) {\n return (\n <PageLayout\n title=\"Work Patterns\"\n description=\"Analyze your work habits and productivity patterns\"\n >\n <LoadingSkeleton variant=\"card\" count={4} />\n </PageLayout>\n );\n }\n\n if (error) {\n return (\n <PageLayout\n title=\"Work Patterns\"\n description=\"Analyze your work habits and productivity patterns\"\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 work patterns. Please try again later.\n </div>\n </PageLayout>\n );\n }\n\n return (\n <PageLayout\n title=\"Work Patterns\"\n description=\"Analyze your work habits and productivity patterns\"\n purpose=\"Learn how you actually work across a typical week — when you focus best, where meetings pile up, and which day is your peak — so you can schedule deep work for the hours you're sharpest.\"\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=\"Productivity Score\"\n value={productivityScore}\n icon={<TrendingUp className=\"h-5 w-5\" />}\n trend={\n summary?.averageProductivity != null\n ? { value: 5, label: \"vs last week\" }\n : undefined\n }\n />\n <StatCard\n title=\"Focus Hours\"\n value={focusHours}\n icon={<Brain className=\"h-5 w-5\" />}\n description={`Out of ${totalHours}h total`}\n />\n <StatCard\n title=\"Peak Day\"\n value={mostProductiveDay}\n icon={<Zap className=\"h-5 w-5\" />}\n description=\"Most productive day\"\n />\n <StatCard\n title=\"Deep Work\"\n value={`${deepWorkSessions} sessions`}\n icon={<Clock className=\"h-5 w-5\" />}\n description=\"90+ min uninterrupted\"\n />\n </div>\n\n {/* Burnout signal — honest, derived from recent stress/energy + insights */}\n <GlassCard\n treatment=\"glass\"\n glow={burnoutTone !== \"success\"}\n className=\"p-5 sm:p-6\"\n >\n <div className=\"flex flex-col gap-4 sm:flex-row sm:items-start\">\n <div\n className={`flex h-11 w-11 shrink-0 items-center justify-center rounded-xl ${burnoutAccent}`}\n >\n {burnoutTone === \"success\" ? (\n <ShieldCheck className=\"h-5 w-5\" />\n ) : (\n <AlertTriangle className=\"h-5 w-5\" />\n )}\n </div>\n <div className=\"min-w-0 flex-1\">\n <div className=\"flex flex-wrap items-center gap-2\">\n <h3 className=\"text-base font-semibold text-text-primary\">\n Burnout Signal\n </h3>\n <StatusBadge\n variant={\n burnoutTone === \"danger\"\n ? \"error\"\n : burnoutTone === \"warning\"\n ? \"warning\"\n : \"success\"\n }\n label={burnoutHeading}\n />\n </div>\n <p className=\"mt-1.5 text-sm text-text-secondary\">\n {burnoutBody}\n </p>\n {burnoutRecommendation && (\n <p className=\"mt-2 flex items-start gap-1.5 text-sm text-text-primary\">\n <Lightbulb className=\"mt-0.5 h-4 w-4 shrink-0 text-primary\" />\n <span>{burnoutRecommendation}</span>\n </p>\n )}\n </div>\n </div>\n </GlassCard>\n\n <div className=\"grid grid-cols-1 gap-6 lg:grid-cols-2\">\n <ChartCard\n title=\"Weekly Productive Hours\"\n description=\"Focused hours logged by weekday across recent weeks\"\n >\n <Heatmap\n data={heatmapData}\n rowLabels={[...DAY_LABELS]}\n columnLabels={columnLabels}\n rowsLabel=\"Day\"\n valueFormat={formatHoursValue}\n legendLabels={[\"Fewer\", \"More\"]}\n height={240}\n emptyLabel=\"No work-pattern days logged yet.\"\n ariaLabel=\"Focused hours by weekday and week\"\n />\n </ChartCard>\n\n <ChartCard\n title=\"Energy & Mood Trends\"\n description=\"Self-reported energy and mood, each on a 1 (low) – 5 (high) scale\"\n >\n {hasEnergyMood ? (\n <TrendChart\n data={trendData}\n xKey=\"date\"\n series={[\n { key: \"energy\", label: \"Energy\" },\n { key: \"mood\", label: \"Mood\" },\n ]}\n type=\"area\"\n height={240}\n emptyLabel=\"No energy or mood check-ins yet.\"\n ariaLabel=\"Energy and mood over time\"\n />\n ) : (\n <EmptyState\n title=\"No energy or mood data yet\"\n description=\"Log how you feel on your work-pattern entries to see the trend here.\"\n />\n )}\n </ChartCard>\n </div>\n\n {insights.length > 0 && (\n <GlassCard treatment=\"glass\" className=\"p-5 sm:p-6\">\n <h3 className=\"mb-4 flex items-center gap-2 text-sm font-semibold text-text-primary\">\n <Lightbulb className=\"h-4 w-4 text-primary\" />\n Insights\n </h3>\n <div className=\"space-y-3\">\n {insights.map((insight: WorkPatternInsight, idx: number) => (\n <div\n key={`${insight.type}-${idx}`}\n className=\"rounded-lg border border-border-subtle bg-bg-surface/40 p-4\"\n >\n <div className=\"flex flex-wrap items-center gap-2\">\n <StatusBadge\n variant={INSIGHT_VARIANT[insight.type]}\n label={INSIGHT_LABEL[insight.type]}\n />\n <p className=\"text-sm font-medium text-text-primary\">\n {insight.title}\n </p>\n </div>\n <p className=\"mt-1.5 text-sm text-text-secondary\">\n {insight.description}\n </p>\n {insight.recommendation && (\n <p className=\"mt-2 flex items-start gap-1.5 text-xs text-text-primary\">\n <ArrowRight className=\"mt-0.5 h-3.5 w-3.5 shrink-0 text-primary\" />\n <span>{insight.recommendation}</span>\n </p>\n )}\n </div>\n ))}\n </div>\n </GlassCard>\n )}\n </div>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
@@ -1,6 +1,5 @@
1
1
  import { FC, PropsWithChildren } from 'react';
2
2
  import { TimeCampusRootProps } from '../types';
3
-
4
3
  /**
5
4
  * Hook to access TimeCampus context
6
5
  */
@@ -1 +1 @@
1
- {"version":3,"file":"TimeCampusProvider.d.ts","sourceRoot":"","sources":["../../../src/timecampus/providers/TimeCampusProvider.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EAAE,EAAE,iBAAiB,EAAE,MAAM,OAAO,CAAC;AAWnD,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,UAAU,CAAC;AASpD;;GAEG;AACH,eAAO,MAAM,aAAa,2BAMzB,CAAC;AAEF;;;;;;;;GAQG;AACH,eAAO,MAAM,qBAAqB,eAKvB,MAAM,SAWhB,CAAC;AAEF;;;;;;;;;GASG;AACH,eAAO,MAAM,kBAAkB,EAAE,EAAE,CAAC,iBAAiB,CAAC,mBAAmB,CAAC,CAuHzE,CAAC"}
1
+ {"version":3,"file":"TimeCampusProvider.d.ts","sourceRoot":"","sources":["../../../src/timecampus/providers/TimeCampusProvider.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EAAE,EAAE,iBAAiB,EAAE,MAAM,OAAO,CAAC;AAQnD,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,UAAU,CAAC;AASpD;;GAEG;AACH,eAAO,MAAM,aAAa,2BAMzB,CAAC;AAEF;;;;;;;;GAQG;AACH,eAAO,MAAM,qBAAqB,eAKvB,MAAM,SAWhB,CAAC;AAEF;;;;;;;;;GASG;AACH,eAAO,MAAM,kBAAkB,EAAE,EAAE,CAAC,iBAAiB,CAAC,mBAAmB,CAAC,CAuHzE,CAAC"}