@burdenoff/microfe-timecampus 2026.703.5 → 2026.703.7

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.
@@ -38,6 +38,64 @@ var k = (k) => {
38
38
  }
39
39
  ]
40
40
  },
41
+ {
42
+ id: "timecampus-life",
43
+ label: "Life",
44
+ path: N(M, "life-timeline"),
45
+ icon: /* @__PURE__ */ D(m, { size: 20 }),
46
+ order: 23,
47
+ group: "main",
48
+ children: [
49
+ {
50
+ id: "timecampus-life-timeline",
51
+ label: "Life Timeline",
52
+ path: N(M, "life-timeline"),
53
+ order: 1
54
+ },
55
+ {
56
+ id: "timecampus-life-balance",
57
+ label: "Life Balance",
58
+ path: N(M, "life-balance"),
59
+ order: 2
60
+ },
61
+ {
62
+ id: "timecampus-finite-life",
63
+ label: "Life in Weeks",
64
+ path: N(M, "finite-life"),
65
+ order: 3
66
+ },
67
+ {
68
+ id: "timecampus-time-well-spent",
69
+ label: "Time Well Spent",
70
+ path: N(M, "time-well-spent"),
71
+ order: 4
72
+ },
73
+ {
74
+ id: "timecampus-dead-time-radar",
75
+ label: "Dead-Time Radar",
76
+ path: N(M, "dead-time-radar"),
77
+ order: 5
78
+ },
79
+ {
80
+ id: "timecampus-values-goals",
81
+ label: "Values & Goals",
82
+ path: N(M, "values-goals"),
83
+ order: 6
84
+ },
85
+ {
86
+ id: "timecampus-recommendations",
87
+ label: "Recommendations",
88
+ path: N(M, "recommendations"),
89
+ order: 7
90
+ },
91
+ {
92
+ id: "timecampus-personal-corporate",
93
+ label: "Personal & Corporate",
94
+ path: N(M, "personal-corporate"),
95
+ order: 8
96
+ }
97
+ ]
98
+ },
41
99
  {
42
100
  id: "timecampus-time",
43
101
  label: "Time Tracking",
@@ -318,64 +376,6 @@ var k = (k) => {
318
376
  icon: /* @__PURE__ */ D(d, { size: 20 }),
319
377
  order: 22,
320
378
  group: "platform"
321
- },
322
- {
323
- id: "timecampus-life",
324
- label: "Life",
325
- path: N(M, "life-timeline"),
326
- icon: /* @__PURE__ */ D(m, { size: 20 }),
327
- order: 5,
328
- group: "main",
329
- children: [
330
- {
331
- id: "timecampus-life-timeline",
332
- label: "Life Timeline",
333
- path: N(M, "life-timeline"),
334
- order: 1
335
- },
336
- {
337
- id: "timecampus-life-balance",
338
- label: "Life Balance",
339
- path: N(M, "life-balance"),
340
- order: 2
341
- },
342
- {
343
- id: "timecampus-finite-life",
344
- label: "Life in Weeks",
345
- path: N(M, "finite-life"),
346
- order: 3
347
- },
348
- {
349
- id: "timecampus-time-well-spent",
350
- label: "Time Well Spent",
351
- path: N(M, "time-well-spent"),
352
- order: 4
353
- },
354
- {
355
- id: "timecampus-dead-time-radar",
356
- label: "Dead-Time Radar",
357
- path: N(M, "dead-time-radar"),
358
- order: 5
359
- },
360
- {
361
- id: "timecampus-values-goals",
362
- label: "Values & Goals",
363
- path: N(M, "values-goals"),
364
- order: 6
365
- },
366
- {
367
- id: "timecampus-recommendations",
368
- label: "Recommendations",
369
- path: N(M, "recommendations"),
370
- order: 7
371
- },
372
- {
373
- id: "timecampus-personal-corporate",
374
- label: "Personal & Corporate",
375
- path: N(M, "personal-corporate"),
376
- order: 8
377
- }
378
- ]
379
379
  }
380
380
  ]);
381
381
  }, [A, M]), r(() => {
@@ -1 +1 @@
1
- {"version":3,"file":"TimeCampusRoot.js","names":[],"sources":["../../src/timecampus/TimeCampusRoot.tsx"],"sourcesContent":["/**\n * TimeCampus Root Component\n * @module timecampus\n */\n\nimport type { FC } from \"react\";\nimport { useEffect } from \"react\";\nimport {\n LayoutDashboard,\n Clock,\n FolderOpen,\n Timer,\n Users,\n CalendarRange,\n CheckCircle,\n Activity,\n TrendingUp,\n Brain,\n Monitor,\n Mail,\n CalendarDays,\n MapPin,\n Receipt,\n Building2,\n Radio,\n LayoutGrid,\n GitBranch,\n Puzzle,\n Sparkles,\n Factory,\n Heart,\n} from \"lucide-react\";\nimport { TimeCampusProvider } from \"./providers/TimeCampusProvider\";\nimport { FeatureFlagProvider } from \"@burdenoff/fe-libs/shared/feature-flags\";\nimport { TimeCampusRoutes } from \"./TimeCampusRoutes\";\nimport { ErrorBoundary } from \"./components/ErrorBoundary\";\nimport type { TimeCampusRootProps, NavItem } from \"./types\";\n\nexport const TimeCampusRoot: FC<TimeCampusRootProps> = (props) => {\n const { registerNavItems, registerFooterItems, basePath = \"/\" } = props;\n\n // Helper to join paths correctly\n const joinPath = (base: string, path: string): string => {\n const cleanBase = base.endsWith(\"/\") ? base.slice(0, -1) : base;\n const cleanPath = path.startsWith(\"/\") ? path : `/${path}`;\n return cleanBase + cleanPath;\n };\n\n // Register navigation items with shell on mount\n useEffect(() => {\n if (!registerNavItems) return;\n\n const navItems: NavItem[] = [\n // 1. Dashboard\n {\n id: \"timecampus-dashboard\",\n label: \"Dashboard\",\n path: joinPath(basePath, \"\"),\n icon: <LayoutDashboard size={20} />,\n order: 1,\n group: \"main\",\n children: [\n {\n id: \"timecampus-dashboard-overview\",\n label: \"Overview\",\n path: joinPath(basePath, \"\"),\n order: 1,\n },\n {\n id: \"timecampus-dashboard-analytics\",\n label: \"Analytics\",\n path: joinPath(basePath, \"analytics\"),\n order: 2,\n },\n {\n id: \"timecampus-dashboard-reports\",\n label: \"Reports\",\n path: joinPath(basePath, \"reports\"),\n order: 3,\n },\n ],\n },\n\n // 2. Time Tracking\n {\n id: \"timecampus-time\",\n label: \"Time Tracking\",\n path: joinPath(basePath, \"time\"),\n icon: <Clock size={20} />,\n order: 2,\n group: \"main\",\n children: [\n {\n id: \"timecampus-time-tracking\",\n label: \"Time Tracking\",\n path: joinPath(basePath, \"time\"),\n order: 1,\n },\n {\n id: \"timecampus-time-timer\",\n label: \"Timer\",\n path: joinPath(basePath, \"time/timer\"),\n order: 2,\n },\n {\n id: \"timecampus-time-entries\",\n label: \"Time Entries\",\n path: joinPath(basePath, \"time/entries\"),\n order: 3,\n },\n ],\n },\n\n // 3. Projects\n {\n id: \"timecampus-projects\",\n label: \"Projects\",\n path: joinPath(basePath, \"projects\"),\n icon: <FolderOpen size={20} />,\n order: 3,\n group: \"main\",\n },\n\n // 4. Pomodoro\n {\n id: \"timecampus-pomodoro\",\n label: \"Pomodoro\",\n path: joinPath(basePath, \"pomodoro\"),\n icon: <Timer size={20} />,\n order: 4,\n group: \"main\",\n children: [\n {\n id: \"timecampus-pomodoro-timer\",\n label: \"Timer\",\n path: joinPath(basePath, \"pomodoro\"),\n order: 1,\n },\n {\n id: \"timecampus-pomodoro-analytics\",\n label: \"Analytics\",\n path: joinPath(basePath, \"pomodoro/analytics\"),\n order: 2,\n },\n {\n id: \"timecampus-pomodoro-settings\",\n label: \"Settings\",\n path: joinPath(basePath, \"pomodoro/settings\"),\n order: 3,\n },\n ],\n },\n\n // 5. Team\n {\n id: \"timecampus-team\",\n label: \"Team\",\n path: joinPath(basePath, \"team\"),\n icon: <Users size={20} />,\n order: 5,\n group: \"main\",\n children: [\n {\n id: \"timecampus-team-overview\",\n label: \"Overview\",\n path: joinPath(basePath, \"team\"),\n order: 1,\n },\n {\n id: \"timecampus-team-members\",\n label: \"Members\",\n path: joinPath(basePath, \"team/members\"),\n order: 2,\n },\n {\n id: \"timecampus-team-analytics\",\n label: \"Analytics\",\n path: joinPath(basePath, \"team/analytics\"),\n order: 3,\n },\n ],\n },\n\n // 6. Resource Planning\n {\n id: \"timecampus-resource-planning\",\n label: \"Resource Planning\",\n path: joinPath(basePath, \"resource-planning\"),\n icon: <CalendarRange size={20} />,\n order: 6,\n group: \"management\",\n },\n\n // 7. Time Approval\n {\n id: \"timecampus-time-approval\",\n label: \"Time Approval\",\n path: joinPath(basePath, \"time-approval\"),\n icon: <CheckCircle size={20} />,\n order: 7,\n group: \"management\",\n },\n\n // 8. Team Activity\n {\n id: \"timecampus-team-activity\",\n label: \"Team Activity\",\n path: joinPath(basePath, \"team-activity\"),\n icon: <Activity size={20} />,\n order: 8,\n group: \"insights\",\n },\n\n // 9. Work Patterns\n {\n id: \"timecampus-work-patterns\",\n label: \"Work Patterns\",\n path: joinPath(basePath, \"work-patterns\"),\n icon: <TrendingUp size={20} />,\n order: 9,\n group: \"insights\",\n },\n\n // 10. Focus Score\n {\n id: \"timecampus-focus-score\",\n label: \"Focus Score\",\n path: joinPath(basePath, \"focus-score\"),\n icon: <Brain size={20} />,\n order: 10,\n group: \"insights\",\n },\n\n // 11. App Usage\n {\n id: \"timecampus-app-usage\",\n label: \"App Usage\",\n path: joinPath(basePath, \"application-usage\"),\n icon: <Monitor size={20} />,\n order: 11,\n group: \"insights\",\n },\n\n // 12. Email Tracking\n {\n id: \"timecampus-email-tracking\",\n label: \"Email Tracking\",\n path: joinPath(basePath, \"email-tracking\"),\n icon: <Mail size={20} />,\n order: 12,\n group: \"tracking\",\n children: [\n {\n id: \"timecampus-email-all\",\n label: \"All Emails\",\n path: joinPath(basePath, \"email-tracking\"),\n order: 1,\n },\n {\n id: \"timecampus-email-analytics\",\n label: \"Analytics\",\n path: joinPath(basePath, \"email-tracking/analytics\"),\n order: 2,\n },\n {\n id: \"timecampus-email-settings\",\n label: \"Settings\",\n path: joinPath(basePath, \"email-tracking/settings\"),\n order: 3,\n },\n ],\n },\n\n // 13. Meetings\n {\n id: \"timecampus-meetings\",\n label: \"Meetings\",\n path: joinPath(basePath, \"meetings\"),\n icon: <CalendarDays size={20} />,\n order: 13,\n group: \"tracking\",\n children: [\n {\n id: \"timecampus-meetings-all\",\n label: \"All Meetings\",\n path: joinPath(basePath, \"meetings\"),\n order: 1,\n },\n {\n id: \"timecampus-meetings-analytics\",\n label: \"Analytics\",\n path: joinPath(basePath, \"meetings/analytics\"),\n order: 2,\n },\n ],\n },\n\n // 14. Location Tracking\n {\n id: \"timecampus-location\",\n label: \"Location Tracking\",\n path: joinPath(basePath, \"location-tracking\"),\n icon: <MapPin size={20} />,\n order: 14,\n group: \"tracking\",\n children: [\n {\n id: \"timecampus-location-all\",\n label: \"Locations\",\n path: joinPath(basePath, \"location-tracking\"),\n order: 1,\n },\n {\n id: \"timecampus-location-geofencing\",\n label: \"Geofencing\",\n path: joinPath(basePath, \"geofencing\"),\n order: 2,\n },\n ],\n },\n\n // 15. Expense Tracking\n {\n id: \"timecampus-expense-tracking\",\n label: \"Expense Tracking\",\n path: joinPath(basePath, \"expense-tracking\"),\n icon: <Receipt size={20} />,\n order: 15,\n group: \"business\",\n },\n\n // 16. Client Portal\n {\n id: \"timecampus-client-portal\",\n label: \"Client Portal\",\n path: joinPath(basePath, \"client-portal\"),\n icon: <Building2 size={20} />,\n order: 16,\n group: \"business\",\n },\n\n // 17. Channels\n {\n id: \"timecampus-channels\",\n label: \"Channels\",\n path: joinPath(basePath, \"channels\"),\n icon: <Radio size={20} />,\n order: 17,\n group: \"platform\",\n },\n\n // 18. Widgets\n {\n id: \"timecampus-widgets\",\n label: \"Widgets\",\n path: joinPath(basePath, \"widgets\"),\n icon: <LayoutGrid size={20} />,\n order: 18,\n group: \"platform\",\n },\n\n // 19. Workflows\n {\n id: \"timecampus-workflows\",\n label: \"Workflows\",\n path: joinPath(basePath, \"workflows\"),\n icon: <GitBranch size={20} />,\n order: 19,\n group: \"platform\",\n },\n\n // 20. Applets\n {\n id: \"timecampus-applets\",\n label: \"Applets\",\n path: joinPath(basePath, \"applets\"),\n icon: <Puzzle size={20} />,\n order: 20,\n group: \"platform\",\n children: [\n {\n id: \"timecampus-applets-installed\",\n label: \"Installed\",\n path: joinPath(basePath, \"applets\"),\n order: 1,\n },\n {\n id: \"timecampus-applets-store\",\n label: \"Store\",\n path: joinPath(basePath, \"applets/store\"),\n order: 2,\n },\n ],\n },\n\n // 21. Templates\n {\n id: \"timecampus-templates\",\n label: \"Templates\",\n path: joinPath(basePath, \"templates\"),\n icon: <Sparkles size={20} />,\n order: 21,\n group: \"platform\",\n },\n\n // 22. Industry Solutions\n {\n id: \"timecampus-industry-solutions\",\n label: \"Industry Solutions\",\n path: joinPath(basePath, \"industry-solutions\"),\n icon: <Factory size={20} />,\n order: 22,\n group: \"platform\",\n },\n\n // Life — the whole of your time (work + personal)\n {\n id: \"timecampus-life\",\n label: \"Life\",\n path: joinPath(basePath, \"life-timeline\"),\n icon: <Heart size={20} />,\n order: 5,\n group: \"main\",\n children: [\n {\n id: \"timecampus-life-timeline\",\n label: \"Life Timeline\",\n path: joinPath(basePath, \"life-timeline\"),\n order: 1,\n },\n {\n id: \"timecampus-life-balance\",\n label: \"Life Balance\",\n path: joinPath(basePath, \"life-balance\"),\n order: 2,\n },\n {\n id: \"timecampus-finite-life\",\n label: \"Life in Weeks\",\n path: joinPath(basePath, \"finite-life\"),\n order: 3,\n },\n {\n id: \"timecampus-time-well-spent\",\n label: \"Time Well Spent\",\n path: joinPath(basePath, \"time-well-spent\"),\n order: 4,\n },\n {\n id: \"timecampus-dead-time-radar\",\n label: \"Dead-Time Radar\",\n path: joinPath(basePath, \"dead-time-radar\"),\n order: 5,\n },\n {\n id: \"timecampus-values-goals\",\n label: \"Values & Goals\",\n path: joinPath(basePath, \"values-goals\"),\n order: 6,\n },\n {\n id: \"timecampus-recommendations\",\n label: \"Recommendations\",\n path: joinPath(basePath, \"recommendations\"),\n order: 7,\n },\n {\n id: \"timecampus-personal-corporate\",\n label: \"Personal & Corporate\",\n path: joinPath(basePath, \"personal-corporate\"),\n order: 8,\n },\n ],\n },\n ];\n\n const cleanup = registerNavItems(navItems);\n return cleanup;\n }, [registerNavItems, basePath]);\n\n // Register footer items\n useEffect(() => {\n if (!registerFooterItems) return;\n\n const footerItems = [\n {\n id: \"timecampus-status\",\n label: \"TimeCampus Ready\",\n type: \"status\" as const,\n order: 10,\n section: \"left\" as const,\n },\n ];\n\n const cleanup = registerFooterItems(footerItems);\n return cleanup;\n }, [registerFooterItems]);\n\n return (\n <FeatureFlagProvider\n workspaceId={null}\n gateway=\"global\"\n defaultEnabled={true}\n >\n <TimeCampusProvider {...props}>\n <ErrorBoundary>\n <TimeCampusRoutes />\n </ErrorBoundary>\n </TimeCampusProvider>\n </FeatureFlagProvider>\n );\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"TimeCampusRoot.js","names":[],"sources":["../../src/timecampus/TimeCampusRoot.tsx"],"sourcesContent":["/**\n * TimeCampus Root Component\n * @module timecampus\n */\n\nimport type { FC } from \"react\";\nimport { useEffect } from \"react\";\nimport {\n LayoutDashboard,\n Clock,\n FolderOpen,\n Timer,\n Users,\n CalendarRange,\n CheckCircle,\n Activity,\n TrendingUp,\n Brain,\n Monitor,\n Mail,\n CalendarDays,\n MapPin,\n Receipt,\n Building2,\n Radio,\n LayoutGrid,\n GitBranch,\n Puzzle,\n Sparkles,\n Factory,\n Heart,\n} from \"lucide-react\";\nimport { TimeCampusProvider } from \"./providers/TimeCampusProvider\";\nimport { FeatureFlagProvider } from \"@burdenoff/fe-libs/shared/feature-flags\";\nimport { TimeCampusRoutes } from \"./TimeCampusRoutes\";\nimport { ErrorBoundary } from \"./components/ErrorBoundary\";\nimport type { TimeCampusRootProps, NavItem } from \"./types\";\n\nexport const TimeCampusRoot: FC<TimeCampusRootProps> = (props) => {\n const { registerNavItems, registerFooterItems, basePath = \"/\" } = props;\n\n // Helper to join paths correctly\n const joinPath = (base: string, path: string): string => {\n const cleanBase = base.endsWith(\"/\") ? base.slice(0, -1) : base;\n const cleanPath = path.startsWith(\"/\") ? path : `/${path}`;\n return cleanBase + cleanPath;\n };\n\n // Register navigation items with shell on mount\n useEffect(() => {\n if (!registerNavItems) return;\n\n const navItems: NavItem[] = [\n // 1. Dashboard\n {\n id: \"timecampus-dashboard\",\n label: \"Dashboard\",\n path: joinPath(basePath, \"\"),\n icon: <LayoutDashboard size={20} />,\n order: 1,\n group: \"main\",\n children: [\n {\n id: \"timecampus-dashboard-overview\",\n label: \"Overview\",\n path: joinPath(basePath, \"\"),\n order: 1,\n },\n {\n id: \"timecampus-dashboard-analytics\",\n label: \"Analytics\",\n path: joinPath(basePath, \"analytics\"),\n order: 2,\n },\n {\n id: \"timecampus-dashboard-reports\",\n label: \"Reports\",\n path: joinPath(basePath, \"reports\"),\n order: 3,\n },\n ],\n },\n\n // Life — the whole of your time (work + personal). Unique order (23) — the\n // shell keys nav items by order within a group, so it must not collide.\n {\n id: \"timecampus-life\",\n label: \"Life\",\n path: joinPath(basePath, \"life-timeline\"),\n icon: <Heart size={20} />,\n order: 23,\n group: \"main\",\n children: [\n {\n id: \"timecampus-life-timeline\",\n label: \"Life Timeline\",\n path: joinPath(basePath, \"life-timeline\"),\n order: 1,\n },\n {\n id: \"timecampus-life-balance\",\n label: \"Life Balance\",\n path: joinPath(basePath, \"life-balance\"),\n order: 2,\n },\n {\n id: \"timecampus-finite-life\",\n label: \"Life in Weeks\",\n path: joinPath(basePath, \"finite-life\"),\n order: 3,\n },\n {\n id: \"timecampus-time-well-spent\",\n label: \"Time Well Spent\",\n path: joinPath(basePath, \"time-well-spent\"),\n order: 4,\n },\n {\n id: \"timecampus-dead-time-radar\",\n label: \"Dead-Time Radar\",\n path: joinPath(basePath, \"dead-time-radar\"),\n order: 5,\n },\n {\n id: \"timecampus-values-goals\",\n label: \"Values & Goals\",\n path: joinPath(basePath, \"values-goals\"),\n order: 6,\n },\n {\n id: \"timecampus-recommendations\",\n label: \"Recommendations\",\n path: joinPath(basePath, \"recommendations\"),\n order: 7,\n },\n {\n id: \"timecampus-personal-corporate\",\n label: \"Personal & Corporate\",\n path: joinPath(basePath, \"personal-corporate\"),\n order: 8,\n },\n ],\n },\n\n // 2. Time Tracking\n {\n id: \"timecampus-time\",\n label: \"Time Tracking\",\n path: joinPath(basePath, \"time\"),\n icon: <Clock size={20} />,\n order: 2,\n group: \"main\",\n children: [\n {\n id: \"timecampus-time-tracking\",\n label: \"Time Tracking\",\n path: joinPath(basePath, \"time\"),\n order: 1,\n },\n {\n id: \"timecampus-time-timer\",\n label: \"Timer\",\n path: joinPath(basePath, \"time/timer\"),\n order: 2,\n },\n {\n id: \"timecampus-time-entries\",\n label: \"Time Entries\",\n path: joinPath(basePath, \"time/entries\"),\n order: 3,\n },\n ],\n },\n\n // 3. Projects\n {\n id: \"timecampus-projects\",\n label: \"Projects\",\n path: joinPath(basePath, \"projects\"),\n icon: <FolderOpen size={20} />,\n order: 3,\n group: \"main\",\n },\n\n // 4. Pomodoro\n {\n id: \"timecampus-pomodoro\",\n label: \"Pomodoro\",\n path: joinPath(basePath, \"pomodoro\"),\n icon: <Timer size={20} />,\n order: 4,\n group: \"main\",\n children: [\n {\n id: \"timecampus-pomodoro-timer\",\n label: \"Timer\",\n path: joinPath(basePath, \"pomodoro\"),\n order: 1,\n },\n {\n id: \"timecampus-pomodoro-analytics\",\n label: \"Analytics\",\n path: joinPath(basePath, \"pomodoro/analytics\"),\n order: 2,\n },\n {\n id: \"timecampus-pomodoro-settings\",\n label: \"Settings\",\n path: joinPath(basePath, \"pomodoro/settings\"),\n order: 3,\n },\n ],\n },\n\n // 5. Team\n {\n id: \"timecampus-team\",\n label: \"Team\",\n path: joinPath(basePath, \"team\"),\n icon: <Users size={20} />,\n order: 5,\n group: \"main\",\n children: [\n {\n id: \"timecampus-team-overview\",\n label: \"Overview\",\n path: joinPath(basePath, \"team\"),\n order: 1,\n },\n {\n id: \"timecampus-team-members\",\n label: \"Members\",\n path: joinPath(basePath, \"team/members\"),\n order: 2,\n },\n {\n id: \"timecampus-team-analytics\",\n label: \"Analytics\",\n path: joinPath(basePath, \"team/analytics\"),\n order: 3,\n },\n ],\n },\n\n // 6. Resource Planning\n {\n id: \"timecampus-resource-planning\",\n label: \"Resource Planning\",\n path: joinPath(basePath, \"resource-planning\"),\n icon: <CalendarRange size={20} />,\n order: 6,\n group: \"management\",\n },\n\n // 7. Time Approval\n {\n id: \"timecampus-time-approval\",\n label: \"Time Approval\",\n path: joinPath(basePath, \"time-approval\"),\n icon: <CheckCircle size={20} />,\n order: 7,\n group: \"management\",\n },\n\n // 8. Team Activity\n {\n id: \"timecampus-team-activity\",\n label: \"Team Activity\",\n path: joinPath(basePath, \"team-activity\"),\n icon: <Activity size={20} />,\n order: 8,\n group: \"insights\",\n },\n\n // 9. Work Patterns\n {\n id: \"timecampus-work-patterns\",\n label: \"Work Patterns\",\n path: joinPath(basePath, \"work-patterns\"),\n icon: <TrendingUp size={20} />,\n order: 9,\n group: \"insights\",\n },\n\n // 10. Focus Score\n {\n id: \"timecampus-focus-score\",\n label: \"Focus Score\",\n path: joinPath(basePath, \"focus-score\"),\n icon: <Brain size={20} />,\n order: 10,\n group: \"insights\",\n },\n\n // 11. App Usage\n {\n id: \"timecampus-app-usage\",\n label: \"App Usage\",\n path: joinPath(basePath, \"application-usage\"),\n icon: <Monitor size={20} />,\n order: 11,\n group: \"insights\",\n },\n\n // 12. Email Tracking\n {\n id: \"timecampus-email-tracking\",\n label: \"Email Tracking\",\n path: joinPath(basePath, \"email-tracking\"),\n icon: <Mail size={20} />,\n order: 12,\n group: \"tracking\",\n children: [\n {\n id: \"timecampus-email-all\",\n label: \"All Emails\",\n path: joinPath(basePath, \"email-tracking\"),\n order: 1,\n },\n {\n id: \"timecampus-email-analytics\",\n label: \"Analytics\",\n path: joinPath(basePath, \"email-tracking/analytics\"),\n order: 2,\n },\n {\n id: \"timecampus-email-settings\",\n label: \"Settings\",\n path: joinPath(basePath, \"email-tracking/settings\"),\n order: 3,\n },\n ],\n },\n\n // 13. Meetings\n {\n id: \"timecampus-meetings\",\n label: \"Meetings\",\n path: joinPath(basePath, \"meetings\"),\n icon: <CalendarDays size={20} />,\n order: 13,\n group: \"tracking\",\n children: [\n {\n id: \"timecampus-meetings-all\",\n label: \"All Meetings\",\n path: joinPath(basePath, \"meetings\"),\n order: 1,\n },\n {\n id: \"timecampus-meetings-analytics\",\n label: \"Analytics\",\n path: joinPath(basePath, \"meetings/analytics\"),\n order: 2,\n },\n ],\n },\n\n // 14. Location Tracking\n {\n id: \"timecampus-location\",\n label: \"Location Tracking\",\n path: joinPath(basePath, \"location-tracking\"),\n icon: <MapPin size={20} />,\n order: 14,\n group: \"tracking\",\n children: [\n {\n id: \"timecampus-location-all\",\n label: \"Locations\",\n path: joinPath(basePath, \"location-tracking\"),\n order: 1,\n },\n {\n id: \"timecampus-location-geofencing\",\n label: \"Geofencing\",\n path: joinPath(basePath, \"geofencing\"),\n order: 2,\n },\n ],\n },\n\n // 15. Expense Tracking\n {\n id: \"timecampus-expense-tracking\",\n label: \"Expense Tracking\",\n path: joinPath(basePath, \"expense-tracking\"),\n icon: <Receipt size={20} />,\n order: 15,\n group: \"business\",\n },\n\n // 16. Client Portal\n {\n id: \"timecampus-client-portal\",\n label: \"Client Portal\",\n path: joinPath(basePath, \"client-portal\"),\n icon: <Building2 size={20} />,\n order: 16,\n group: \"business\",\n },\n\n // 17. Channels\n {\n id: \"timecampus-channels\",\n label: \"Channels\",\n path: joinPath(basePath, \"channels\"),\n icon: <Radio size={20} />,\n order: 17,\n group: \"platform\",\n },\n\n // 18. Widgets\n {\n id: \"timecampus-widgets\",\n label: \"Widgets\",\n path: joinPath(basePath, \"widgets\"),\n icon: <LayoutGrid size={20} />,\n order: 18,\n group: \"platform\",\n },\n\n // 19. Workflows\n {\n id: \"timecampus-workflows\",\n label: \"Workflows\",\n path: joinPath(basePath, \"workflows\"),\n icon: <GitBranch size={20} />,\n order: 19,\n group: \"platform\",\n },\n\n // 20. Applets\n {\n id: \"timecampus-applets\",\n label: \"Applets\",\n path: joinPath(basePath, \"applets\"),\n icon: <Puzzle size={20} />,\n order: 20,\n group: \"platform\",\n children: [\n {\n id: \"timecampus-applets-installed\",\n label: \"Installed\",\n path: joinPath(basePath, \"applets\"),\n order: 1,\n },\n {\n id: \"timecampus-applets-store\",\n label: \"Store\",\n path: joinPath(basePath, \"applets/store\"),\n order: 2,\n },\n ],\n },\n\n // 21. Templates\n {\n id: \"timecampus-templates\",\n label: \"Templates\",\n path: joinPath(basePath, \"templates\"),\n icon: <Sparkles size={20} />,\n order: 21,\n group: \"platform\",\n },\n\n // 22. Industry Solutions\n {\n id: \"timecampus-industry-solutions\",\n label: \"Industry Solutions\",\n path: joinPath(basePath, \"industry-solutions\"),\n icon: <Factory size={20} />,\n order: 22,\n group: \"platform\",\n },\n ];\n\n const cleanup = registerNavItems(navItems);\n return cleanup;\n }, [registerNavItems, basePath]);\n\n // Register footer items\n useEffect(() => {\n if (!registerFooterItems) return;\n\n const footerItems = [\n {\n id: \"timecampus-status\",\n label: \"TimeCampus Ready\",\n type: \"status\" as const,\n order: 10,\n section: \"left\" as const,\n },\n ];\n\n const cleanup = registerFooterItems(footerItems);\n return cleanup;\n }, [registerFooterItems]);\n\n return (\n <FeatureFlagProvider\n workspaceId={null}\n gateway=\"global\"\n defaultEnabled={true}\n >\n <TimeCampusProvider {...props}>\n <ErrorBoundary>\n <TimeCampusRoutes />\n </ErrorBoundary>\n </TimeCampusProvider>\n </FeatureFlagProvider>\n );\n};\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"BalanceWheel.js","names":[],"sources":["../../../../src/timecampus/components/charts/BalanceWheel.tsx"],"sourcesContent":["import {\n PolarAngleAxis,\n PolarGrid,\n Radar,\n RadarChart,\n ResponsiveContainer,\n Tooltip,\n} from \"recharts\";\nimport { chartTokens, usePrefersReducedMotion } from \"./chartTheme\";\nimport { ChartEmpty, ChartTooltip, srOnlyStyle } from \"./ChartParts\";\n\n/** One life-domain axis of the wheel: the actual vs the desired share of time. */\nexport interface BalanceWheelDatum {\n /** Stable domain key (e.g. \"FAMILY\"). */\n domain: string;\n /** Human label shown on the axis (e.g. \"Family\"). */\n label: string;\n /** Actual share of covered time, 0..1. */\n actualShare: number;\n /** Desired share the user declared, 0..1. Omit when no target is set. */\n desiredShare?: number | null;\n}\n\nexport interface BalanceWheelProps {\n data: BalanceWheelDatum[];\n /** Pixel height of the wheel. Default 320. */\n height?: number;\n /** Message when there is no covered time to plot. */\n emptyLabel?: string;\n ariaLabel?: string;\n className?: string;\n}\n\n/**\n * BalanceWheel — the signature life-balance visual: a radar of how your finite\n * time actually splits across life domains (solid brand polygon) overlaid with\n * the balance you said you WANT (dashed muted polygon). The gap between the two\n * is the insight. Token-only, theme-aware, and renders an honest empty state —\n * never a fake polygon — when there is no covered time. Values are real shares\n * passed in by the page; this component never fabricates data.\n */\nexport function BalanceWheel({\n data,\n height = 320,\n emptyLabel,\n ariaLabel,\n className,\n}: BalanceWheelProps) {\n const reducedMotion = usePrefersReducedMotion();\n\n const rows = data.filter(\n (d) => Number.isFinite(d.actualShare) && d.actualShare >= 0,\n );\n const total = rows.reduce((sum, d) => sum + d.actualShare, 0);\n const hasDesired = rows.some(\n (d) => typeof d.desiredShare === \"number\" && Number.isFinite(d.desiredShare),\n );\n\n if (rows.length === 0 || total <= 0) {\n return <ChartEmpty height={height} label={emptyLabel} className={className} />;\n }\n\n const chartData = rows.map((d) => ({\n label: d.label,\n Actual: Math.round(d.actualShare * 1000) / 10,\n Desired:\n typeof d.desiredShare === \"number\" && Number.isFinite(d.desiredShare)\n ? Math.round(d.desiredShare * 1000) / 10\n : 0,\n }));\n\n const label =\n ariaLabel ?? `Life balance across ${rows.map((d) => d.label).join(\", \")}`;\n\n return (\n <figure role=\"img\" aria-label={label} className={className}>\n <div style={{ height }}>\n <ResponsiveContainer width=\"100%\" height=\"100%\">\n <RadarChart data={chartData} outerRadius=\"72%\">\n <PolarGrid stroke={chartTokens.grid} />\n <PolarAngleAxis\n dataKey=\"label\"\n tick={{ fill: chartTokens.textSecondary, fontSize: 11 }}\n />\n <Tooltip\n cursor={false}\n content={<ChartTooltip valueFormat={(v) => `${v}%`} />}\n />\n {hasDesired && (\n <Radar\n name=\"Desired\"\n dataKey=\"Desired\"\n stroke={chartTokens.textMuted}\n strokeDasharray=\"4 3\"\n strokeWidth={1.5}\n fill=\"transparent\"\n isAnimationActive={!reducedMotion}\n />\n )}\n <Radar\n name=\"Actual\"\n dataKey=\"Actual\"\n stroke={chartTokens.brand}\n strokeWidth={2}\n fill={chartTokens.brand}\n fillOpacity={0.28}\n isAnimationActive={!reducedMotion}\n />\n </RadarChart>\n </ResponsiveContainer>\n </div>\n\n <ul className=\"mt-2 flex flex-wrap items-center justify-center gap-x-4 gap-y-1.5 text-xs text-text-secondary\">\n <li className=\"flex items-center gap-1.5\">\n <span\n aria-hidden=\"true\"\n className=\"h-2.5 w-2.5 rounded-[3px]\"\n style={{ backgroundColor: chartTokens.brand }}\n />\n Actual\n </li>\n {hasDesired && (\n <li className=\"flex items-center gap-1.5\">\n <span\n aria-hidden=\"true\"\n className=\"h-0 w-3.5 border-t-2 border-dashed\"\n style={{ borderColor: chartTokens.textMuted }}\n />\n Desired\n </li>\n )}\n </ul>\n\n <table style={srOnlyStyle}>\n <caption>{label}</caption>\n <thead>\n <tr>\n <th scope=\"col\">Domain</th>\n <th scope=\"col\">Actual share</th>\n {hasDesired && <th scope=\"col\">Desired share</th>}\n </tr>\n </thead>\n <tbody>\n {rows.map((d) => (\n <tr key={d.domain}>\n <th scope=\"row\">{d.label}</th>\n <td>{`${Math.round(d.actualShare * 100)}%`}</td>\n {hasDesired && (\n <td>\n {typeof d.desiredShare === \"number\"\n ? `${Math.round(d.desiredShare * 100)}%`\n : \"—\"}\n </td>\n )}\n </tr>\n ))}\n </tbody>\n </table>\n </figure>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"BalanceWheel.js","names":[],"sources":["../../../../src/timecampus/components/charts/BalanceWheel.tsx"],"sourcesContent":["import {\n PolarAngleAxis,\n PolarGrid,\n Radar,\n RadarChart,\n ResponsiveContainer,\n Tooltip,\n} from \"recharts\";\nimport { chartTokens, usePrefersReducedMotion } from \"./chartTheme\";\nimport { ChartEmpty, ChartTooltip, srOnlyStyle } from \"./ChartParts\";\n\n/** One life-domain axis of the wheel: the actual vs the desired share of time. */\nexport interface BalanceWheelDatum {\n /** Stable domain key (e.g. \"FAMILY\"). */\n domain: string;\n /** Human label shown on the axis (e.g. \"Family\"). */\n label: string;\n /** Actual share of covered time, 0..1. */\n actualShare: number;\n /** Desired share the user declared, 0..1. Omit when no target is set. */\n desiredShare?: number | null;\n}\n\nexport interface BalanceWheelProps {\n data: BalanceWheelDatum[];\n /** Pixel height of the wheel. Default 320. */\n height?: number;\n /** Message when there is no covered time to plot. */\n emptyLabel?: string;\n ariaLabel?: string;\n className?: string;\n}\n\n/**\n * BalanceWheel — the signature life-balance visual: a radar of how your finite\n * time actually splits across life domains (solid brand polygon) overlaid with\n * the balance you said you WANT (dashed muted polygon). The gap between the two\n * is the insight. Token-only, theme-aware, and renders an honest empty state —\n * never a fake polygon — when there is no covered time. Values are real shares\n * passed in by the page; this component never fabricates data.\n */\nexport function BalanceWheel({\n data,\n height = 320,\n emptyLabel,\n ariaLabel,\n className,\n}: BalanceWheelProps) {\n const reducedMotion = usePrefersReducedMotion();\n\n const rows = data.filter(\n (d) => Number.isFinite(d.actualShare) && d.actualShare >= 0,\n );\n const total = rows.reduce((sum, d) => sum + d.actualShare, 0);\n const hasDesired = rows.some(\n (d) =>\n typeof d.desiredShare === \"number\" && Number.isFinite(d.desiredShare),\n );\n\n if (rows.length === 0 || total <= 0) {\n return (\n <ChartEmpty height={height} label={emptyLabel} className={className} />\n );\n }\n\n const chartData = rows.map((d) => ({\n label: d.label,\n Actual: Math.round(d.actualShare * 1000) / 10,\n Desired:\n typeof d.desiredShare === \"number\" && Number.isFinite(d.desiredShare)\n ? Math.round(d.desiredShare * 1000) / 10\n : 0,\n }));\n\n const label =\n ariaLabel ?? `Life balance across ${rows.map((d) => d.label).join(\", \")}`;\n\n return (\n <figure role=\"img\" aria-label={label} className={className}>\n <div style={{ height }}>\n <ResponsiveContainer width=\"100%\" height=\"100%\">\n <RadarChart data={chartData} outerRadius=\"72%\">\n <PolarGrid stroke={chartTokens.grid} />\n <PolarAngleAxis\n dataKey=\"label\"\n tick={{ fill: chartTokens.textSecondary, fontSize: 11 }}\n />\n <Tooltip\n cursor={false}\n content={<ChartTooltip valueFormat={(v) => `${v}%`} />}\n />\n {hasDesired && (\n <Radar\n name=\"Desired\"\n dataKey=\"Desired\"\n stroke={chartTokens.textMuted}\n strokeDasharray=\"4 3\"\n strokeWidth={1.5}\n fill=\"transparent\"\n isAnimationActive={!reducedMotion}\n />\n )}\n <Radar\n name=\"Actual\"\n dataKey=\"Actual\"\n stroke={chartTokens.brand}\n strokeWidth={2}\n fill={chartTokens.brand}\n fillOpacity={0.28}\n isAnimationActive={!reducedMotion}\n />\n </RadarChart>\n </ResponsiveContainer>\n </div>\n\n <ul className=\"mt-2 flex flex-wrap items-center justify-center gap-x-4 gap-y-1.5 text-xs text-text-secondary\">\n <li className=\"flex items-center gap-1.5\">\n <span\n aria-hidden=\"true\"\n className=\"h-2.5 w-2.5 rounded-[3px]\"\n style={{ backgroundColor: chartTokens.brand }}\n />\n Actual\n </li>\n {hasDesired && (\n <li className=\"flex items-center gap-1.5\">\n <span\n aria-hidden=\"true\"\n className=\"h-0 w-3.5 border-t-2 border-dashed\"\n style={{ borderColor: chartTokens.textMuted }}\n />\n Desired\n </li>\n )}\n </ul>\n\n <table style={srOnlyStyle}>\n <caption>{label}</caption>\n <thead>\n <tr>\n <th scope=\"col\">Domain</th>\n <th scope=\"col\">Actual share</th>\n {hasDesired && <th scope=\"col\">Desired share</th>}\n </tr>\n </thead>\n <tbody>\n {rows.map((d) => (\n <tr key={d.domain}>\n <th scope=\"row\">{d.label}</th>\n <td>{`${Math.round(d.actualShare * 100)}%`}</td>\n {hasDesired && (\n <td>\n {typeof d.desiredShare === \"number\"\n ? `${Math.round(d.desiredShare * 100)}%`\n : \"—\"}\n </td>\n )}\n </tr>\n ))}\n </tbody>\n </table>\n </figure>\n );\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"WeeksGrid.js","names":[],"sources":["../../../../src/timecampus/components/charts/WeeksGrid.tsx"],"sourcesContent":["import { useMemo } from \"react\";\nimport { chartTokens } from \"./chartTheme\";\n\nexport interface WeeksGridProps {\n /** Whole weeks lived so far. Null when the user has not given a birth date. */\n weeksLived: number | null;\n /** Total weeks in the expected lifespan. Null when no expectancy is given. */\n totalWeeks: number | null;\n /** Columns per row. Default 52 (one year per row). */\n weeksPerRow?: number;\n /**\n * When there is no expectancy, how many future rows of hollow cells to show\n * as a horizon beyond the weeks lived. Default 12 (about a decade of context).\n */\n horizonRowsWhenOpen?: number;\n ariaLabel?: string;\n className?: string;\n}\n\n/**\n * WeeksGrid — the finite-life \"your life in weeks\" memento. Each cell is one\n * week: weeks lived are filled with the brand token, the current week is ringed,\n * and future weeks are hollow. It is honest by construction — it renders only\n * what the user's own (optional, private) birth date and expectancy imply, never\n * a guessed age or an invented mortality date. Drawn as a single SVG so even a\n * ~90-year grid (≈ 4,700 cells) stays light, and it scales responsively via a\n * viewBox inside a horizontally-scrollable frame.\n */\nexport function WeeksGrid({\n weeksLived,\n totalWeeks,\n weeksPerRow = 52,\n horizonRowsWhenOpen = 12,\n ariaLabel,\n className,\n}: WeeksGridProps) {\n const lived = weeksLived != null && weeksLived >= 0 ? Math.floor(weeksLived) : 0;\n\n const totalCells = useMemo(() => {\n if (totalWeeks != null && totalWeeks > 0) return Math.floor(totalWeeks);\n // No expectancy: show the weeks lived plus a modest hollow horizon.\n return lived + Math.max(1, horizonRowsWhenOpen) * weeksPerRow;\n }, [totalWeeks, lived, horizonRowsWhenOpen, weeksPerRow]);\n\n const cols = Math.max(1, weeksPerRow);\n const rows = Math.max(1, Math.ceil(totalCells / cols));\n\n // Geometry in SVG user units; the viewBox lets it scale to any width.\n const cell = 10;\n const gap = 3;\n const step = cell + gap;\n const width = cols * step - gap;\n const heightUnits = rows * step - gap;\n\n const cells = useMemo(() => {\n const out: {\n i: number;\n x: number;\n y: number;\n state: \"lived\" | \"current\" | \"future\";\n }[] = [];\n for (let i = 0; i < totalCells; i++) {\n const state = i < lived ? \"lived\" : i === lived ? \"current\" : \"future\";\n out.push({\n i,\n x: (i % cols) * step,\n y: Math.floor(i / cols) * step,\n state,\n });\n }\n return out;\n }, [totalCells, lived, cols, step]);\n\n const label =\n ariaLabel ??\n (totalWeeks != null\n ? `Your life in weeks: ${lived} of ${Math.floor(totalWeeks)} weeks lived`\n : `Your life in weeks: ${lived} weeks lived so far`);\n\n return (\n <figure role=\"img\" aria-label={label} className={className}>\n <div className=\"w-full overflow-x-auto\">\n <svg\n viewBox={`0 0 ${width} ${heightUnits}`}\n width=\"100%\"\n preserveAspectRatio=\"xMinYMin meet\"\n className=\"min-w-[520px]\"\n role=\"presentation\"\n >\n {cells.map((c) => {\n const isLived = c.state === \"lived\";\n const isCurrent = c.state === \"current\";\n return (\n <rect\n key={c.i}\n x={c.x}\n y={c.y}\n width={cell}\n height={cell}\n rx={2}\n ry={2}\n fill={\n isLived\n ? chartTokens.brand\n : isCurrent\n ? chartTokens.surface\n : \"transparent\"\n }\n stroke={\n isCurrent\n ? chartTokens.brand\n : isLived\n ? \"transparent\"\n : chartTokens.border\n }\n strokeWidth={isCurrent ? 1.5 : 1}\n />\n );\n })}\n </svg>\n </div>\n </figure>\n );\n}\n"],"mappings":";;;;AA4BA,SAAgB,EAAU,EACxB,eACA,eACA,iBAAc,IACd,yBAAsB,IACtB,cACA,gBACiB;CACjB,IAAM,IAAQ,KAAc,QAAQ,KAAc,IAAI,KAAK,MAAM,EAAW,GAAG,GAEzE,IAAa,QACb,KAAc,QAAQ,IAAa,IAAU,KAAK,MAAM,EAAW,GAEhE,IAAQ,KAAK,IAAI,GAAG,EAAoB,GAAG,GACjD;EAAC;EAAY;EAAO;EAAqB;EAAY,CAAC,EAEnD,IAAO,KAAK,IAAI,GAAG,EAAY,EAC/B,IAAO,KAAK,IAAI,GAAG,KAAK,KAAK,IAAa,EAAK,CAAC,EAMhD,IAAQ,IAAO,KAAO,GACtB,IAAc,IAAO,KAAO,GAE5B,IAAQ,QAAc;EAC1B,IAAM,IAKA,EAAE;AACR,OAAK,IAAI,IAAI,GAAG,IAAI,GAAY,KAAK;GACnC,IAAM,IAAQ,IAAI,IAAQ,UAAU,MAAM,IAAQ,YAAY;AAC9D,KAAI,KAAK;IACP;IACA,GAAI,IAAI,IAAQ;IAChB,GAAG,KAAK,MAAM,IAAI,EAAK,GAAG;IAC1B;IACD,CAAC;;AAEJ,SAAO;IACN;EAAC;EAAY;EAAO;EAAM;EAAK,CAAC;AAQnC,QACE,kBAAC,UAAD;EAAQ,MAAK;EAAM,cANnB,MACC,KAAc,OAEX,uBAAuB,EAAM,uBAD7B,uBAAuB,EAAM,MAAM,KAAK,MAAM,EAAW,CAAC;EAIb;YAC/C,kBAAC,OAAD;GAAK,WAAU;aACb,kBAAC,OAAD;IACE,SAAS,OAAO,EAAM,GAAG;IACzB,OAAM;IACN,qBAAoB;IACpB,WAAU;IACV,MAAK;cAEJ,EAAM,KAAK,MAAM;KAChB,IAAM,IAAU,EAAE,UAAU,SACtB,IAAY,EAAE,UAAU;AAC9B,YACE,kBAAC,QAAD;MAEE,GAAG,EAAE;MACL,GAAG,EAAE;MACL,OAAO;MACP,QAAQ;MACR,IAAI;MACJ,IAAI;MACJ,MACE,IACI,EAAY,QACZ,IACE,EAAY,UACZ;MAER,QACE,IACI,EAAY,QACZ,IACE,gBACA,EAAY;MAEpB,aAAa,IAAY,MAAM;MAC/B,EAtBK,EAAE,EAsBP;MAEJ;IACE,CAAA;GACF,CAAA;EACC,CAAA"}
1
+ {"version":3,"file":"WeeksGrid.js","names":[],"sources":["../../../../src/timecampus/components/charts/WeeksGrid.tsx"],"sourcesContent":["import { useMemo } from \"react\";\nimport { chartTokens } from \"./chartTheme\";\n\nexport interface WeeksGridProps {\n /** Whole weeks lived so far. Null when the user has not given a birth date. */\n weeksLived: number | null;\n /** Total weeks in the expected lifespan. Null when no expectancy is given. */\n totalWeeks: number | null;\n /** Columns per row. Default 52 (one year per row). */\n weeksPerRow?: number;\n /**\n * When there is no expectancy, how many future rows of hollow cells to show\n * as a horizon beyond the weeks lived. Default 12 (about a decade of context).\n */\n horizonRowsWhenOpen?: number;\n ariaLabel?: string;\n className?: string;\n}\n\n/**\n * WeeksGrid — the finite-life \"your life in weeks\" memento. Each cell is one\n * week: weeks lived are filled with the brand token, the current week is ringed,\n * and future weeks are hollow. It is honest by construction — it renders only\n * what the user's own (optional, private) birth date and expectancy imply, never\n * a guessed age or an invented mortality date. Drawn as a single SVG so even a\n * ~90-year grid (≈ 4,700 cells) stays light, and it scales responsively via a\n * viewBox inside a horizontally-scrollable frame.\n */\nexport function WeeksGrid({\n weeksLived,\n totalWeeks,\n weeksPerRow = 52,\n horizonRowsWhenOpen = 12,\n ariaLabel,\n className,\n}: WeeksGridProps) {\n const lived =\n weeksLived != null && weeksLived >= 0 ? Math.floor(weeksLived) : 0;\n\n const totalCells = useMemo(() => {\n if (totalWeeks != null && totalWeeks > 0) return Math.floor(totalWeeks);\n // No expectancy: show the weeks lived plus a modest hollow horizon.\n return lived + Math.max(1, horizonRowsWhenOpen) * weeksPerRow;\n }, [totalWeeks, lived, horizonRowsWhenOpen, weeksPerRow]);\n\n const cols = Math.max(1, weeksPerRow);\n const rows = Math.max(1, Math.ceil(totalCells / cols));\n\n // Geometry in SVG user units; the viewBox lets it scale to any width.\n const cell = 10;\n const gap = 3;\n const step = cell + gap;\n const width = cols * step - gap;\n const heightUnits = rows * step - gap;\n\n const cells = useMemo(() => {\n const out: {\n i: number;\n x: number;\n y: number;\n state: \"lived\" | \"current\" | \"future\";\n }[] = [];\n for (let i = 0; i < totalCells; i++) {\n const state = i < lived ? \"lived\" : i === lived ? \"current\" : \"future\";\n out.push({\n i,\n x: (i % cols) * step,\n y: Math.floor(i / cols) * step,\n state,\n });\n }\n return out;\n }, [totalCells, lived, cols, step]);\n\n const label =\n ariaLabel ??\n (totalWeeks != null\n ? `Your life in weeks: ${lived} of ${Math.floor(totalWeeks)} weeks lived`\n : `Your life in weeks: ${lived} weeks lived so far`);\n\n return (\n <figure role=\"img\" aria-label={label} className={className}>\n <div className=\"w-full overflow-x-auto\">\n <svg\n viewBox={`0 0 ${width} ${heightUnits}`}\n width=\"100%\"\n preserveAspectRatio=\"xMinYMin meet\"\n className=\"min-w-[520px]\"\n role=\"presentation\"\n >\n {cells.map((c) => {\n const isLived = c.state === \"lived\";\n const isCurrent = c.state === \"current\";\n return (\n <rect\n key={c.i}\n x={c.x}\n y={c.y}\n width={cell}\n height={cell}\n rx={2}\n ry={2}\n fill={\n isLived\n ? chartTokens.brand\n : isCurrent\n ? chartTokens.surface\n : \"transparent\"\n }\n stroke={\n isCurrent\n ? chartTokens.brand\n : isLived\n ? \"transparent\"\n : chartTokens.border\n }\n strokeWidth={isCurrent ? 1.5 : 1}\n />\n );\n })}\n </svg>\n </div>\n </figure>\n );\n}\n"],"mappings":";;;;AA4BA,SAAgB,EAAU,EACxB,eACA,eACA,iBAAc,IACd,yBAAsB,IACtB,cACA,gBACiB;CACjB,IAAM,IACJ,KAAc,QAAQ,KAAc,IAAI,KAAK,MAAM,EAAW,GAAG,GAE7D,IAAa,QACb,KAAc,QAAQ,IAAa,IAAU,KAAK,MAAM,EAAW,GAEhE,IAAQ,KAAK,IAAI,GAAG,EAAoB,GAAG,GACjD;EAAC;EAAY;EAAO;EAAqB;EAAY,CAAC,EAEnD,IAAO,KAAK,IAAI,GAAG,EAAY,EAC/B,IAAO,KAAK,IAAI,GAAG,KAAK,KAAK,IAAa,EAAK,CAAC,EAMhD,IAAQ,IAAO,KAAO,GACtB,IAAc,IAAO,KAAO,GAE5B,IAAQ,QAAc;EAC1B,IAAM,IAKA,EAAE;AACR,OAAK,IAAI,IAAI,GAAG,IAAI,GAAY,KAAK;GACnC,IAAM,IAAQ,IAAI,IAAQ,UAAU,MAAM,IAAQ,YAAY;AAC9D,KAAI,KAAK;IACP;IACA,GAAI,IAAI,IAAQ;IAChB,GAAG,KAAK,MAAM,IAAI,EAAK,GAAG;IAC1B;IACD,CAAC;;AAEJ,SAAO;IACN;EAAC;EAAY;EAAO;EAAM;EAAK,CAAC;AAQnC,QACE,kBAAC,UAAD;EAAQ,MAAK;EAAM,cANnB,MACC,KAAc,OAEX,uBAAuB,EAAM,uBAD7B,uBAAuB,EAAM,MAAM,KAAK,MAAM,EAAW,CAAC;EAIb;YAC/C,kBAAC,OAAD;GAAK,WAAU;aACb,kBAAC,OAAD;IACE,SAAS,OAAO,EAAM,GAAG;IACzB,OAAM;IACN,qBAAoB;IACpB,WAAU;IACV,MAAK;cAEJ,EAAM,KAAK,MAAM;KAChB,IAAM,IAAU,EAAE,UAAU,SACtB,IAAY,EAAE,UAAU;AAC9B,YACE,kBAAC,QAAD;MAEE,GAAG,EAAE;MACL,GAAG,EAAE;MACL,OAAO;MACP,QAAQ;MACR,IAAI;MACJ,IAAI;MACJ,MACE,IACI,EAAY,QACZ,IACE,EAAY,UACZ;MAER,QACE,IACI,EAAY,QACZ,IACE,gBACA,EAAY;MAEpB,aAAa,IAAY,MAAM;MAC/B,EAtBK,EAAE,EAsBP;MAEJ;IACE,CAAA;GACF,CAAA;EACC,CAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"AnalyticsPage.js","names":[],"sources":["../../../../src/timecampus/pages/dashboard/AnalyticsPage.tsx"],"sourcesContent":["import {\n Clock,\n DollarSign,\n Brain,\n TrendingUp,\n Gauge,\n PieChart as PieIcon,\n CalendarRange,\n Lightbulb,\n} from \"lucide-react\";\nimport { NextSteps } from \"@burdenoff/fe-libs/ui\";\nimport { PageLayout } from \"../../components/PageLayout\";\nimport { LoadingSkeleton } from \"../../components/LoadingSkeleton\";\nimport {\n StatCard,\n StatusBadge,\n ChartCard,\n RadialGauge,\n DonutChart,\n BarsChart,\n type DonutSlice,\n} from \"../../components\";\nimport { formatHours } from \"../../utils/helpers\";\nimport {\n useTimeCampus,\n useTimeCampusNavigate,\n} from \"../../providers/TimeCampusProvider\";\nimport {\n WorkPatternInsightType,\n type WorkPatternInsight,\n} from \"@/generated/wspace-types\";\nimport {\n useGetTimeEntryStatsQuery,\n useGetFocusScoreSummaryQuery,\n useGetWorkPatternSummaryQuery,\n} from \"@/generated/wspace-operations\";\n\ntype BadgeVariant = \"success\" | \"warning\" | \"error\" | \"info\" | \"muted\";\n\nconst INSIGHT_META: Record<\n WorkPatternInsightType,\n { variant: BadgeVariant; label: string }\n> = {\n [WorkPatternInsightType.Positive]: { variant: \"success\", label: \"Positive\" },\n [WorkPatternInsightType.Negative]: { variant: \"error\", label: \"Needs work\" },\n [WorkPatternInsightType.Warning]: { variant: \"warning\", label: \"Warning\" },\n [WorkPatternInsightType.Recommendation]: { variant: \"info\", label: \"Tip\" },\n [WorkPatternInsightType.Neutral]: { variant: \"muted\", label: \"Insight\" },\n};\n\nexport function AnalyticsPage() {\n const { workspaceId = \"\" } = useTimeCampus();\n const navigate = useTimeCampusNavigate();\n\n const {\n data: statsData,\n loading: statsLoading,\n error: statsError,\n } = useGetTimeEntryStatsQuery({\n skip: !workspaceId,\n });\n\n const { data: focusData, loading: focusLoading } =\n useGetFocusScoreSummaryQuery({\n skip: !workspaceId,\n });\n\n const { data: patternData, loading: patternLoading } =\n useGetWorkPatternSummaryQuery({\n skip: !workspaceId,\n });\n\n const stats = statsData?.timeEntryStats;\n const focus = focusData?.focusScoreSummary;\n const pattern = patternData?.workPatternSummary;\n\n if (statsLoading || focusLoading || patternLoading) {\n return (\n <PageLayout\n title=\"Analytics\"\n description=\"Insights into your time tracking and productivity\"\n >\n <LoadingSkeleton variant=\"card\" count={4} />\n </PageLayout>\n );\n }\n\n if (statsError) {\n return (\n <PageLayout\n title=\"Analytics\"\n description=\"Insights into your time tracking and productivity\"\n >\n <div className=\"rounded-lg border border-status-error-bg/40 bg-status-error-bg p-4 text-sm text-status-error-text\">\n Failed to load analytics. Please try again later.\n </div>\n </PageLayout>\n );\n }\n\n // Billable vs non-billable split from the real time-entry aggregate.\n const billableHours = stats\n ? Math.max(0, Math.round(stats.billableHours))\n : 0;\n const nonBillableHours = stats\n ? Math.max(0, Math.round(stats.totalHours - stats.billableHours))\n : 0;\n const billablePct =\n stats && stats.totalHours > 0\n ? Math.round((stats.billableHours / stats.totalHours) * 100)\n : 0;\n const distributionSlices: DonutSlice[] = [\n { name: \"Billable\", value: billableHours },\n { name: \"Non-billable\", value: nonBillableHours },\n ];\n\n // Average-day split from the real work-pattern aggregate.\n const dayBreakdown = pattern\n ? [\n { label: \"Focus\", hours: Math.max(0, pattern.averageFocusHours) },\n { label: \"Meetings\", hours: Math.max(0, pattern.averageMeetingHours) },\n ]\n : [];\n\n const trendText = focus?.trend\n ? focus.trend.charAt(0).toUpperCase() + focus.trend.slice(1)\n : undefined;\n\n return (\n <PageLayout\n title=\"Analytics\"\n description=\"Insights into your time tracking and productivity\"\n purpose=\"Turn the raw hours you track into answers — spot whether your focus is improving, where billable time is going, and which habits to change to get more done with less burnout.\"\n >\n <NextSteps\n storageKey=\"timecampus-analytics\"\n className=\"mb-6\"\n steps={[\n {\n id: \"reports\",\n label: \"Build a detailed report\",\n description: \"Slice time by project, client or person.\",\n onClick: () => navigate(\"/reports\"),\n },\n {\n id: \"focus\",\n label: \"Dig into your focus score\",\n description: \"Understand what's helping or hurting deep work.\",\n onClick: () => navigate(\"/focus-score\"),\n },\n {\n id: \"patterns\",\n label: \"Review your work patterns\",\n description: \"See how your week actually breaks down.\",\n onClick: () => navigate(\"/work-patterns\"),\n },\n ]}\n />\n\n {/* KPI row */}\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4\">\n <StatCard\n title=\"Total Hours\"\n value={stats ? formatHours(stats.totalHours) : \"--\"}\n icon={<Clock className=\"h-5 w-5\" />}\n description={stats ? `${stats.totalEntries} entries` : undefined}\n />\n <StatCard\n title=\"Billable Hours\"\n value={stats ? formatHours(stats.billableHours) : \"--\"}\n icon={<DollarSign className=\"h-5 w-5\" />}\n description={stats ? `${billablePct}% of tracked time` : undefined}\n iconBgColor=\"bg-status-success-bg text-status-success-text\"\n />\n <StatCard\n featured\n title=\"Avg Focus Score\"\n value={focus ? `${Math.round(focus.averageScore)}` : \"--\"}\n icon={<Brain className=\"h-5 w-5\" />}\n description={trendText ? `${trendText} trend` : undefined}\n iconBgColor=\"bg-status-info-bg text-status-info-text\"\n />\n <StatCard\n title=\"Avg Productivity\"\n value={pattern ? `${Math.round(pattern.averageProductivity)}%` : \"--\"}\n icon={<TrendingUp className=\"h-5 w-5\" />}\n description={\n stats && stats.activeTimers > 0\n ? `${stats.activeTimers} timer(s) running`\n : undefined\n }\n />\n </div>\n\n {/* Focus gauge + time distribution */}\n <div className=\"mt-6 grid grid-cols-1 gap-6 lg:grid-cols-2\">\n <ChartCard\n title=\"Focus Score\"\n description=\"Your average deep-work score across the tracked window\"\n actions={<Gauge className=\"h-4 w-4 text-text-secondary\" />}\n >\n {focus ? (\n <div className=\"space-y-4\">\n <RadialGauge\n value={focus.averageScore}\n min={0}\n max={100}\n label=\"Avg score\"\n sublabel={trendText ? `${trendText} trend` : undefined}\n thresholds={{ warning: 75, danger: 50 }}\n height={196}\n />\n <div className=\"grid grid-cols-2 gap-3\">\n <div className=\"rounded-lg border border-border-seam bg-bg-sunken/40 p-3\">\n <p className=\"text-xs text-text-secondary\">Lowest</p>\n <p className=\"mt-0.5 text-lg font-semibold tabular-nums text-text-primary\">\n {Math.round(focus.lowestScore)}\n </p>\n </div>\n <div className=\"rounded-lg border border-border-seam bg-bg-sunken/40 p-3\">\n <p className=\"text-xs text-text-secondary\">Highest</p>\n <p className=\"mt-0.5 text-lg font-semibold tabular-nums text-text-primary\">\n {Math.round(focus.highestScore)}\n </p>\n </div>\n </div>\n {(focus.topFactor || focus.improvementArea) && (\n <div className=\"flex flex-wrap items-center gap-2\">\n {focus.topFactor && (\n <StatusBadge\n variant=\"success\"\n label={`Strength: ${focus.topFactor}`}\n />\n )}\n {focus.improvementArea && (\n <StatusBadge\n variant=\"warning\"\n label={`Improve: ${focus.improvementArea}`}\n />\n )}\n </div>\n )}\n </div>\n ) : (\n <p className=\"py-8 text-center text-sm text-text-secondary\">\n No focus score data yet.\n </p>\n )}\n </ChartCard>\n\n <ChartCard\n title=\"Time Distribution\"\n description=\"Billable vs non-billable across everything you tracked\"\n actions={<PieIcon className=\"h-4 w-4 text-text-secondary\" />}\n >\n {stats && stats.totalHours > 0 ? (\n <DonutChart\n data={distributionSlices}\n centerLabel=\"Tracked\"\n centerValue={`${Math.round(stats.totalHours)}h`}\n valueFormat={formatHours}\n height={240}\n ariaLabel=\"Billable versus non-billable hours\"\n />\n ) : (\n <p className=\"py-8 text-center text-sm text-text-secondary\">\n No time entries tracked yet.\n </p>\n )}\n </ChartCard>\n </div>\n\n {/* Work pattern insights */}\n {pattern && (\n <ChartCard\n title=\"Work Patterns\"\n description=\"How your average day splits and what to change\"\n actions={<CalendarRange className=\"h-4 w-4 text-text-secondary\" />}\n className=\"mt-6\"\n >\n <div className=\"grid grid-cols-1 gap-6 lg:grid-cols-2\">\n <div className=\"space-y-4\">\n <div className=\"flex flex-wrap gap-2\">\n {pattern.mostProductiveDay && (\n <StatusBadge\n variant=\"success\"\n label={`Peak: ${pattern.mostProductiveDay}`}\n />\n )}\n {pattern.leastProductiveDay && (\n <StatusBadge\n variant=\"muted\"\n label={`Quietest: ${pattern.leastProductiveDay}`}\n />\n )}\n </div>\n {dayBreakdown.some((d) => d.hours > 0) ? (\n <BarsChart\n data={dayBreakdown}\n xKey=\"label\"\n series={[{ key: \"hours\", label: \"Avg hours / day\" }]}\n orientation=\"horizontal\"\n valueFormat={formatHours}\n categoryWidth={80}\n height={140}\n ariaLabel=\"Average focus versus meeting hours per day\"\n />\n ) : (\n <p className=\"text-sm text-text-secondary\">\n Not enough tracked days to break down your average day yet.\n </p>\n )}\n </div>\n\n <div className=\"space-y-3\">\n {pattern.insights.length === 0 ? (\n <p className=\"text-sm text-text-secondary\">\n No pattern insights surfaced yet — keep tracking and they'll\n appear here.\n </p>\n ) : (\n pattern.insights.map((insight: WorkPatternInsight, index: number) => {\n const meta = INSIGHT_META[insight.type];\n return (\n <div\n key={`${insight.title}-${index}`}\n className=\"rounded-lg border border-border-subtle bg-bg-sunken/40 p-3\"\n >\n <div className=\"flex items-start justify-between gap-3\">\n <p className=\"text-sm font-medium text-text-primary\">\n {insight.title}\n </p>\n <StatusBadge\n variant={meta.variant}\n label={meta.label}\n />\n </div>\n <p className=\"mt-1 text-xs text-text-secondary\">\n {insight.description}\n </p>\n {insight.recommendation && (\n <p className=\"mt-1.5 flex items-start gap-1.5 text-xs text-text-secondary\">\n <Lightbulb className=\"mt-0.5 h-3.5 w-3.5 shrink-0 text-status-info-text\" />\n <span>{insight.recommendation}</span>\n </p>\n )}\n </div>\n );\n })\n )}\n </div>\n </div>\n </ChartCard>\n )}\n </PageLayout>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"AnalyticsPage.js","names":[],"sources":["../../../../src/timecampus/pages/dashboard/AnalyticsPage.tsx"],"sourcesContent":["import {\n Clock,\n DollarSign,\n Brain,\n TrendingUp,\n Gauge,\n PieChart as PieIcon,\n CalendarRange,\n Lightbulb,\n} from \"lucide-react\";\nimport { NextSteps } from \"@burdenoff/fe-libs/ui\";\nimport { PageLayout } from \"../../components/PageLayout\";\nimport { LoadingSkeleton } from \"../../components/LoadingSkeleton\";\nimport {\n StatCard,\n StatusBadge,\n ChartCard,\n RadialGauge,\n DonutChart,\n BarsChart,\n type DonutSlice,\n} from \"../../components\";\nimport { formatHours } from \"../../utils/helpers\";\nimport {\n useTimeCampus,\n useTimeCampusNavigate,\n} from \"../../providers/TimeCampusProvider\";\nimport {\n WorkPatternInsightType,\n type WorkPatternInsight,\n} from \"@/generated/wspace-types\";\nimport {\n useGetTimeEntryStatsQuery,\n useGetFocusScoreSummaryQuery,\n useGetWorkPatternSummaryQuery,\n} from \"@/generated/wspace-operations\";\n\ntype BadgeVariant = \"success\" | \"warning\" | \"error\" | \"info\" | \"muted\";\n\nconst INSIGHT_META: Record<\n WorkPatternInsightType,\n { variant: BadgeVariant; label: string }\n> = {\n [WorkPatternInsightType.Positive]: { variant: \"success\", label: \"Positive\" },\n [WorkPatternInsightType.Negative]: { variant: \"error\", label: \"Needs work\" },\n [WorkPatternInsightType.Warning]: { variant: \"warning\", label: \"Warning\" },\n [WorkPatternInsightType.Recommendation]: { variant: \"info\", label: \"Tip\" },\n [WorkPatternInsightType.Neutral]: { variant: \"muted\", label: \"Insight\" },\n};\n\nexport function AnalyticsPage() {\n const { workspaceId = \"\" } = useTimeCampus();\n const navigate = useTimeCampusNavigate();\n\n const {\n data: statsData,\n loading: statsLoading,\n error: statsError,\n } = useGetTimeEntryStatsQuery({\n skip: !workspaceId,\n });\n\n const { data: focusData, loading: focusLoading } =\n useGetFocusScoreSummaryQuery({\n skip: !workspaceId,\n });\n\n const { data: patternData, loading: patternLoading } =\n useGetWorkPatternSummaryQuery({\n skip: !workspaceId,\n });\n\n const stats = statsData?.timeEntryStats;\n const focus = focusData?.focusScoreSummary;\n const pattern = patternData?.workPatternSummary;\n\n if (statsLoading || focusLoading || patternLoading) {\n return (\n <PageLayout\n title=\"Analytics\"\n description=\"Insights into your time tracking and productivity\"\n >\n <LoadingSkeleton variant=\"card\" count={4} />\n </PageLayout>\n );\n }\n\n if (statsError) {\n return (\n <PageLayout\n title=\"Analytics\"\n description=\"Insights into your time tracking and productivity\"\n >\n <div className=\"rounded-lg border border-status-error-bg/40 bg-status-error-bg p-4 text-sm text-status-error-text\">\n Failed to load analytics. Please try again later.\n </div>\n </PageLayout>\n );\n }\n\n // Billable vs non-billable split from the real time-entry aggregate.\n const billableHours = stats\n ? Math.max(0, Math.round(stats.billableHours))\n : 0;\n const nonBillableHours = stats\n ? Math.max(0, Math.round(stats.totalHours - stats.billableHours))\n : 0;\n const billablePct =\n stats && stats.totalHours > 0\n ? Math.round((stats.billableHours / stats.totalHours) * 100)\n : 0;\n const distributionSlices: DonutSlice[] = [\n { name: \"Billable\", value: billableHours },\n { name: \"Non-billable\", value: nonBillableHours },\n ];\n\n // Average-day split from the real work-pattern aggregate.\n const dayBreakdown = pattern\n ? [\n { label: \"Focus\", hours: Math.max(0, pattern.averageFocusHours) },\n { label: \"Meetings\", hours: Math.max(0, pattern.averageMeetingHours) },\n ]\n : [];\n\n const trendText = focus?.trend\n ? focus.trend.charAt(0).toUpperCase() + focus.trend.slice(1)\n : undefined;\n\n return (\n <PageLayout\n title=\"Analytics\"\n description=\"Insights into your time tracking and productivity\"\n purpose=\"Turn the raw hours you track into answers — spot whether your focus is improving, where billable time is going, and which habits to change to get more done with less burnout.\"\n >\n <NextSteps\n storageKey=\"timecampus-analytics\"\n className=\"mb-6\"\n steps={[\n {\n id: \"reports\",\n label: \"Build a detailed report\",\n description: \"Slice time by project, client or person.\",\n onClick: () => navigate(\"/reports\"),\n },\n {\n id: \"focus\",\n label: \"Dig into your focus score\",\n description: \"Understand what's helping or hurting deep work.\",\n onClick: () => navigate(\"/focus-score\"),\n },\n {\n id: \"patterns\",\n label: \"Review your work patterns\",\n description: \"See how your week actually breaks down.\",\n onClick: () => navigate(\"/work-patterns\"),\n },\n ]}\n />\n\n {/* KPI row */}\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4\">\n <StatCard\n title=\"Total Hours\"\n value={stats ? formatHours(stats.totalHours) : \"--\"}\n icon={<Clock className=\"h-5 w-5\" />}\n description={stats ? `${stats.totalEntries} entries` : undefined}\n />\n <StatCard\n title=\"Billable Hours\"\n value={stats ? formatHours(stats.billableHours) : \"--\"}\n icon={<DollarSign className=\"h-5 w-5\" />}\n description={stats ? `${billablePct}% of tracked time` : undefined}\n iconBgColor=\"bg-status-success-bg text-status-success-text\"\n />\n <StatCard\n featured\n title=\"Avg Focus Score\"\n value={focus ? `${Math.round(focus.averageScore)}` : \"--\"}\n icon={<Brain className=\"h-5 w-5\" />}\n description={trendText ? `${trendText} trend` : undefined}\n iconBgColor=\"bg-status-info-bg text-status-info-text\"\n />\n <StatCard\n title=\"Avg Productivity\"\n value={pattern ? `${Math.round(pattern.averageProductivity)}%` : \"--\"}\n icon={<TrendingUp className=\"h-5 w-5\" />}\n description={\n stats && stats.activeTimers > 0\n ? `${stats.activeTimers} timer(s) running`\n : undefined\n }\n />\n </div>\n\n {/* Focus gauge + time distribution */}\n <div className=\"mt-6 grid grid-cols-1 gap-6 lg:grid-cols-2\">\n <ChartCard\n title=\"Focus Score\"\n description=\"Your average deep-work score across the tracked window\"\n actions={<Gauge className=\"h-4 w-4 text-text-secondary\" />}\n >\n {focus ? (\n <div className=\"space-y-4\">\n <RadialGauge\n value={focus.averageScore}\n min={0}\n max={100}\n label=\"Avg score\"\n sublabel={trendText ? `${trendText} trend` : undefined}\n thresholds={{ warning: 75, danger: 50 }}\n height={196}\n />\n <div className=\"grid grid-cols-2 gap-3\">\n <div className=\"rounded-lg border border-border-seam bg-bg-sunken/40 p-3\">\n <p className=\"text-xs text-text-secondary\">Lowest</p>\n <p className=\"mt-0.5 text-lg font-semibold tabular-nums text-text-primary\">\n {Math.round(focus.lowestScore)}\n </p>\n </div>\n <div className=\"rounded-lg border border-border-seam bg-bg-sunken/40 p-3\">\n <p className=\"text-xs text-text-secondary\">Highest</p>\n <p className=\"mt-0.5 text-lg font-semibold tabular-nums text-text-primary\">\n {Math.round(focus.highestScore)}\n </p>\n </div>\n </div>\n {(focus.topFactor || focus.improvementArea) && (\n <div className=\"flex flex-wrap items-center gap-2\">\n {focus.topFactor && (\n <StatusBadge\n variant=\"success\"\n label={`Strength: ${focus.topFactor}`}\n />\n )}\n {focus.improvementArea && (\n <StatusBadge\n variant=\"warning\"\n label={`Improve: ${focus.improvementArea}`}\n />\n )}\n </div>\n )}\n </div>\n ) : (\n <p className=\"py-8 text-center text-sm text-text-secondary\">\n No focus score data yet.\n </p>\n )}\n </ChartCard>\n\n <ChartCard\n title=\"Time Distribution\"\n description=\"Billable vs non-billable across everything you tracked\"\n actions={<PieIcon className=\"h-4 w-4 text-text-secondary\" />}\n >\n {stats && stats.totalHours > 0 ? (\n <DonutChart\n data={distributionSlices}\n centerLabel=\"Tracked\"\n centerValue={`${Math.round(stats.totalHours)}h`}\n valueFormat={formatHours}\n height={240}\n ariaLabel=\"Billable versus non-billable hours\"\n />\n ) : (\n <p className=\"py-8 text-center text-sm text-text-secondary\">\n No time entries tracked yet.\n </p>\n )}\n </ChartCard>\n </div>\n\n {/* Work pattern insights */}\n {pattern && (\n <ChartCard\n title=\"Work Patterns\"\n description=\"How your average day splits and what to change\"\n actions={<CalendarRange className=\"h-4 w-4 text-text-secondary\" />}\n className=\"mt-6\"\n >\n <div className=\"grid grid-cols-1 gap-6 lg:grid-cols-2\">\n <div className=\"space-y-4\">\n <div className=\"flex flex-wrap gap-2\">\n {pattern.mostProductiveDay && (\n <StatusBadge\n variant=\"success\"\n label={`Peak: ${pattern.mostProductiveDay}`}\n />\n )}\n {pattern.leastProductiveDay && (\n <StatusBadge\n variant=\"muted\"\n label={`Quietest: ${pattern.leastProductiveDay}`}\n />\n )}\n </div>\n {dayBreakdown.some((d) => d.hours > 0) ? (\n <BarsChart\n data={dayBreakdown}\n xKey=\"label\"\n series={[{ key: \"hours\", label: \"Avg hours / day\" }]}\n orientation=\"horizontal\"\n valueFormat={formatHours}\n categoryWidth={80}\n height={140}\n ariaLabel=\"Average focus versus meeting hours per day\"\n />\n ) : (\n <p className=\"text-sm text-text-secondary\">\n Not enough tracked days to break down your average day yet.\n </p>\n )}\n </div>\n\n <div className=\"space-y-3\">\n {pattern.insights.length === 0 ? (\n <p className=\"text-sm text-text-secondary\">\n No pattern insights surfaced yet — keep tracking and they'll\n appear here.\n </p>\n ) : (\n pattern.insights.map(\n (insight: WorkPatternInsight, index: number) => {\n const meta = INSIGHT_META[insight.type];\n return (\n <div\n key={`${insight.title}-${index}`}\n className=\"rounded-lg border border-border-subtle bg-bg-sunken/40 p-3\"\n >\n <div className=\"flex items-start justify-between gap-3\">\n <p className=\"text-sm font-medium text-text-primary\">\n {insight.title}\n </p>\n <StatusBadge\n variant={meta.variant}\n label={meta.label}\n />\n </div>\n <p className=\"mt-1 text-xs text-text-secondary\">\n {insight.description}\n </p>\n {insight.recommendation && (\n <p className=\"mt-1.5 flex items-start gap-1.5 text-xs text-text-secondary\">\n <Lightbulb className=\"mt-0.5 h-3.5 w-3.5 shrink-0 text-status-info-text\" />\n <span>{insight.recommendation}</span>\n </p>\n )}\n </div>\n );\n },\n )\n )}\n </div>\n </div>\n </ChartCard>\n )}\n </PageLayout>\n );\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"DeadTimeRadarPage.js","names":[],"sources":["../../../../src/timecampus/pages/lifetime/DeadTimeRadarPage.tsx"],"sourcesContent":["import { useMemo, useState, type ComponentType } from \"react\";\nimport {\n Radar,\n Sparkles,\n Clock,\n Percent,\n Info,\n Hourglass,\n Bus,\n CircleHelp,\n TimerOff,\n CircleDashed,\n} from \"lucide-react\";\nimport { GlassCard } from \"@burdenoff/fe-libs/ui\";\nimport {\n PageLayout,\n StatCard,\n ChartCard,\n BarsChart,\n ProgressMeter,\n EmptyState,\n LoadingSkeleton,\n type ChartDatum,\n} from \"../../components\";\nimport { useTimeCampus } from \"../../providers/TimeCampusProvider\";\nimport { useMyDeadTimeRadarQuery } from \"@/generated/wspace-operations\";\nimport { formatSecondsHuman, isoDaysAgo, isoNow } from \"./domains\";\n\n/** Range presets that drive the self-scoped query's `from`/`to` window. */\nconst RANGE_OPTIONS = [\n { label: \"7 days\", value: 7 },\n { label: \"30 days\", value: 30 },\n { label: \"90 days\", value: 90 },\n] as const;\n\n/**\n * Friendly labels + an icon for the friction \"kinds\" the backend emits. The raw\n * kind string is the source of truth; unknown kinds fall back to a humanised\n * version of the string so a new backend category never renders as a raw enum.\n */\nconst KIND_META: Record<string, { label: string; Icon: ComponentType<{ className?: string }> }> = {\n WAITING: { label: \"Queues & waiting\", Icon: Hourglass },\n COMMUTE: { label: \"Commute (dead transit)\", Icon: Bus },\n UNCATEGORISED: { label: \"Uncategorised time\", Icon: CircleHelp },\n UNCATEGORIZED: { label: \"Uncategorised time\", Icon: CircleHelp },\n IDLE_GAP: { label: \"Idle gaps\", Icon: TimerOff },\n IDLE: { label: \"Idle gaps\", Icon: TimerOff },\n GAP: { label: \"Untracked gaps\", Icon: TimerOff },\n};\n\nfunction humaniseKind(kind: string): string {\n return kind\n .toLowerCase()\n .split(/[_\\s]+/)\n .filter(Boolean)\n .map((word) => word.charAt(0).toUpperCase() + word.slice(1))\n .join(\" \");\n}\n\nfunction kindLabel(kind: string): string {\n return KIND_META[kind]?.label ?? humaniseKind(kind);\n}\n\nfunction kindIcon(kind: string): ComponentType<{ className?: string }> {\n return KIND_META[kind]?.Icon ?? CircleDashed;\n}\n\nexport function DeadTimeRadarPage() {\n const { workspaceId } = useTimeCampus();\n const [rangeDays, setRangeDays] = useState<number>(30);\n\n // Memoise the window so `from`/`to` are stable across renders (isoNow() would\n // otherwise produce a fresh string every render and refetch in a loop).\n const { from, to } = useMemo(\n () => ({ from: isoDaysAgo(rangeDays), to: isoNow() }),\n [rangeDays],\n );\n\n const { data, loading, error } = useMyDeadTimeRadarQuery({\n variables: { from, to },\n skip: !workspaceId,\n });\n\n const radar = data?.myDeadTimeRadar;\n\n const rangeSelector = (\n <div\n className=\"flex items-center rounded-lg border border-border-subtle bg-bg-sunken/50 p-0.5\"\n role=\"group\"\n aria-label=\"Time range\"\n >\n {RANGE_OPTIONS.map((option) => {\n const active = option.value === rangeDays;\n return (\n <button\n key={option.value}\n type=\"button\"\n aria-pressed={active}\n onClick={() => setRangeDays(option.value)}\n className={\n active\n ? \"rounded-md bg-bg-surface px-2.5 py-1 text-xs font-medium text-text-primary shadow-sm\"\n : \"rounded-md px-2.5 py-1 text-xs font-medium text-text-secondary transition-colors hover:text-text-primary\"\n }\n >\n {option.label}\n </button>\n );\n })}\n </div>\n );\n\n const purpose =\n \"Dead-Time Radar surfaces the friction hidden in your day — the queues, \" +\n \"commutes and untracked gaps that quietly eat your hours — and quantifies \" +\n \"how much of it you could realistically reclaim.\";\n\n if (!workspaceId || loading) {\n return (\n <PageLayout\n title=\"Dead-Time Radar\"\n description=\"Quantified reclaimable hours from friction and untracked gaps\"\n purpose={purpose}\n actions={rangeSelector}\n >\n <LoadingSkeleton variant=\"card\" count={4} />\n </PageLayout>\n );\n }\n\n if (error) {\n return (\n <PageLayout\n title=\"Dead-Time Radar\"\n description=\"Quantified reclaimable hours from friction and untracked gaps\"\n purpose={purpose}\n actions={rangeSelector}\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 Couldn&apos;t load your dead-time radar right now. Please try again in\n a moment.\n </div>\n </PageLayout>\n );\n }\n\n const buckets = radar?.buckets ?? [];\n const totalSeconds = radar?.totalSeconds ?? 0;\n const reclaimableSeconds = radar?.reclaimableSeconds ?? 0;\n const reclaimablePct =\n totalSeconds > 0\n ? Math.round((reclaimableSeconds / totalSeconds) * 100)\n : 0;\n\n // Largest friction first so the biggest opportunities read at the top.\n const sortedBuckets = [...buckets].sort(\n (a, b) => b.totalSeconds - a.totalSeconds,\n );\n\n const chartData: ChartDatum[] = sortedBuckets.map((bucket) => ({\n kind: kindLabel(bucket.kind),\n total: bucket.totalSeconds,\n reclaimable: bucket.reclaimableSeconds,\n }));\n\n const noBuckets = buckets.length === 0;\n\n return (\n <PageLayout\n title=\"Dead-Time Radar\"\n description=\"Quantified reclaimable hours from friction and untracked gaps\"\n purpose={purpose}\n actions={rangeSelector}\n >\n <div className=\"space-y-6\">\n {/* Hero metrics — all derived from the query result. */}\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3\">\n <StatCard\n featured\n title=\"Reclaimable time\"\n value={formatSecondsHuman(reclaimableSeconds)}\n icon={<Sparkles className=\"h-5 w-5\" />}\n description={`Estimated from your last ${rangeDays} days`}\n />\n <StatCard\n title=\"Total dead-time\"\n value={formatSecondsHuman(totalSeconds)}\n icon={<Clock className=\"h-5 w-5\" />}\n description=\"Friction + untracked gaps logged\"\n />\n <StatCard\n title=\"Reclaimable share\"\n value={`${reclaimablePct}%`}\n icon={<Percent className=\"h-5 w-5\" />}\n description=\"Of total dead-time you could recover\"\n />\n </div>\n\n {/* Honest measurement note — shown prominently, never overstated. */}\n {radar?.note && (\n <GlassCard treatment=\"insight\" className=\"p-4 sm:p-5\">\n <div className=\"flex gap-3\">\n <span className=\"flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-status-info-bg text-status-info-text\">\n <Info className=\"h-5 w-5\" />\n </span>\n <div className=\"min-w-0\">\n <h3 className=\"text-sm font-semibold text-text-primary\">\n How this is measured\n </h3>\n <p className=\"mt-1 text-sm text-text-secondary\">\n {radar.note}\n </p>\n </div>\n </div>\n </GlassCard>\n )}\n\n {noBuckets ? (\n <EmptyState\n title=\"No dead-time detected yet\"\n description=\"Once you log friction moments — time spent waiting, commuting, or on-hold — and leave gaps in your tracked day, your reclaimable hours will show up here.\"\n action={\n <span className=\"inline-flex items-center gap-2 rounded-md bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text\">\n <Radar className=\"h-4 w-4\" />\n Log your first friction moment\n </span>\n }\n />\n ) : (\n <>\n {/* Per-bucket breakdown cards. */}\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3\">\n {sortedBuckets.map((bucket) => {\n const BucketIcon = kindIcon(bucket.kind);\n return (\n <GlassCard\n key={bucket.kind}\n treatment=\"glass\"\n className=\"p-4 sm:p-5\"\n >\n <div className=\"flex items-start justify-between gap-3\">\n <div className=\"min-w-0\">\n <p className=\"truncate text-sm font-medium text-text-secondary\">\n {kindLabel(bucket.kind)}\n </p>\n <p className=\"mt-1.5 text-2xl font-bold tabular-nums text-text-primary\">\n {formatSecondsHuman(bucket.totalSeconds)}\n </p>\n </div>\n <span className=\"flex h-10 w-10 shrink-0 items-center justify-center rounded-lg bg-action-primary-bg/10 text-primary\">\n <BucketIcon className=\"h-5 w-5\" />\n </span>\n </div>\n <div className=\"mt-4\">\n <ProgressMeter\n value={bucket.reclaimableSeconds}\n max={bucket.totalSeconds || 1}\n label=\"Reclaimable\"\n tone=\"warning\"\n size=\"sm\"\n format={(v) => formatSecondsHuman(v)}\n />\n </div>\n </GlassCard>\n );\n })}\n </div>\n\n {/* Total vs reclaimable per friction source. */}\n <ChartCard\n title=\"Friction by source\"\n description=\"Total dead-time against the portion you could reclaim, per source\"\n >\n <BarsChart\n data={chartData}\n xKey=\"kind\"\n orientation=\"horizontal\"\n categoryWidth={140}\n series={[\n { key: \"total\", label: \"Total dead-time\" },\n { key: \"reclaimable\", label: \"Reclaimable\" },\n ]}\n valueFormat={(v) => formatSecondsHuman(v)}\n height={Math.max(220, sortedBuckets.length * 56)}\n emptyLabel=\"No friction sources in this range.\"\n ariaLabel=\"Total and reclaimable dead-time by source\"\n />\n </ChartCard>\n </>\n )}\n </div>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"DeadTimeRadarPage.js","names":[],"sources":["../../../../src/timecampus/pages/lifetime/DeadTimeRadarPage.tsx"],"sourcesContent":["import { useMemo, useState, type ComponentType } from \"react\";\nimport {\n Radar,\n Sparkles,\n Clock,\n Percent,\n Info,\n Hourglass,\n Bus,\n CircleHelp,\n TimerOff,\n CircleDashed,\n} from \"lucide-react\";\nimport { GlassCard } from \"@burdenoff/fe-libs/ui\";\nimport {\n PageLayout,\n StatCard,\n ChartCard,\n BarsChart,\n ProgressMeter,\n EmptyState,\n LoadingSkeleton,\n type ChartDatum,\n} from \"../../components\";\nimport { useTimeCampus } from \"../../providers/TimeCampusProvider\";\nimport { useMyDeadTimeRadarQuery } from \"@/generated/wspace-operations\";\nimport { formatSecondsHuman, isoDaysAgo, isoNow } from \"./domains\";\n\n/** Range presets that drive the self-scoped query's `from`/`to` window. */\nconst RANGE_OPTIONS = [\n { label: \"7 days\", value: 7 },\n { label: \"30 days\", value: 30 },\n { label: \"90 days\", value: 90 },\n] as const;\n\n/**\n * Friendly labels + an icon for the friction \"kinds\" the backend emits. The raw\n * kind string is the source of truth; unknown kinds fall back to a humanised\n * version of the string so a new backend category never renders as a raw enum.\n */\nconst KIND_META: Record<\n string,\n { label: string; Icon: ComponentType<{ className?: string }> }\n> = {\n WAITING: { label: \"Queues & waiting\", Icon: Hourglass },\n COMMUTE: { label: \"Commute (dead transit)\", Icon: Bus },\n UNCATEGORISED: { label: \"Uncategorised time\", Icon: CircleHelp },\n UNCATEGORIZED: { label: \"Uncategorised time\", Icon: CircleHelp },\n IDLE_GAP: { label: \"Idle gaps\", Icon: TimerOff },\n IDLE: { label: \"Idle gaps\", Icon: TimerOff },\n GAP: { label: \"Untracked gaps\", Icon: TimerOff },\n};\n\nfunction humaniseKind(kind: string): string {\n return kind\n .toLowerCase()\n .split(/[_\\s]+/)\n .filter(Boolean)\n .map((word) => word.charAt(0).toUpperCase() + word.slice(1))\n .join(\" \");\n}\n\nfunction kindLabel(kind: string): string {\n return KIND_META[kind]?.label ?? humaniseKind(kind);\n}\n\nfunction kindIcon(kind: string): ComponentType<{ className?: string }> {\n return KIND_META[kind]?.Icon ?? CircleDashed;\n}\n\nexport function DeadTimeRadarPage() {\n const { workspaceId } = useTimeCampus();\n const [rangeDays, setRangeDays] = useState<number>(30);\n\n // Memoise the window so `from`/`to` are stable across renders (isoNow() would\n // otherwise produce a fresh string every render and refetch in a loop).\n const { from, to } = useMemo(\n () => ({ from: isoDaysAgo(rangeDays), to: isoNow() }),\n [rangeDays],\n );\n\n const { data, loading, error } = useMyDeadTimeRadarQuery({\n variables: { from, to },\n skip: !workspaceId,\n });\n\n const radar = data?.myDeadTimeRadar;\n\n const rangeSelector = (\n <div\n className=\"flex items-center rounded-lg border border-border-subtle bg-bg-sunken/50 p-0.5\"\n role=\"group\"\n aria-label=\"Time range\"\n >\n {RANGE_OPTIONS.map((option) => {\n const active = option.value === rangeDays;\n return (\n <button\n key={option.value}\n type=\"button\"\n aria-pressed={active}\n onClick={() => setRangeDays(option.value)}\n className={\n active\n ? \"rounded-md bg-bg-surface px-2.5 py-1 text-xs font-medium text-text-primary shadow-sm\"\n : \"rounded-md px-2.5 py-1 text-xs font-medium text-text-secondary transition-colors hover:text-text-primary\"\n }\n >\n {option.label}\n </button>\n );\n })}\n </div>\n );\n\n const purpose =\n \"Dead-Time Radar surfaces the friction hidden in your day — the queues, \" +\n \"commutes and untracked gaps that quietly eat your hours — and quantifies \" +\n \"how much of it you could realistically reclaim.\";\n\n if (!workspaceId || loading) {\n return (\n <PageLayout\n title=\"Dead-Time Radar\"\n description=\"Quantified reclaimable hours from friction and untracked gaps\"\n purpose={purpose}\n actions={rangeSelector}\n >\n <LoadingSkeleton variant=\"card\" count={4} />\n </PageLayout>\n );\n }\n\n if (error) {\n return (\n <PageLayout\n title=\"Dead-Time Radar\"\n description=\"Quantified reclaimable hours from friction and untracked gaps\"\n purpose={purpose}\n actions={rangeSelector}\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 Couldn&apos;t load your dead-time radar right now. Please try again in\n a moment.\n </div>\n </PageLayout>\n );\n }\n\n const buckets = radar?.buckets ?? [];\n const totalSeconds = radar?.totalSeconds ?? 0;\n const reclaimableSeconds = radar?.reclaimableSeconds ?? 0;\n const reclaimablePct =\n totalSeconds > 0\n ? Math.round((reclaimableSeconds / totalSeconds) * 100)\n : 0;\n\n // Largest friction first so the biggest opportunities read at the top.\n const sortedBuckets = [...buckets].sort(\n (a, b) => b.totalSeconds - a.totalSeconds,\n );\n\n const chartData: ChartDatum[] = sortedBuckets.map((bucket) => ({\n kind: kindLabel(bucket.kind),\n total: bucket.totalSeconds,\n reclaimable: bucket.reclaimableSeconds,\n }));\n\n const noBuckets = buckets.length === 0;\n\n return (\n <PageLayout\n title=\"Dead-Time Radar\"\n description=\"Quantified reclaimable hours from friction and untracked gaps\"\n purpose={purpose}\n actions={rangeSelector}\n >\n <div className=\"space-y-6\">\n {/* Hero metrics — all derived from the query result. */}\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3\">\n <StatCard\n featured\n title=\"Reclaimable time\"\n value={formatSecondsHuman(reclaimableSeconds)}\n icon={<Sparkles className=\"h-5 w-5\" />}\n description={`Estimated from your last ${rangeDays} days`}\n />\n <StatCard\n title=\"Total dead-time\"\n value={formatSecondsHuman(totalSeconds)}\n icon={<Clock className=\"h-5 w-5\" />}\n description=\"Friction + untracked gaps logged\"\n />\n <StatCard\n title=\"Reclaimable share\"\n value={`${reclaimablePct}%`}\n icon={<Percent className=\"h-5 w-5\" />}\n description=\"Of total dead-time you could recover\"\n />\n </div>\n\n {/* Honest measurement note — shown prominently, never overstated. */}\n {radar?.note && (\n <GlassCard treatment=\"insight\" className=\"p-4 sm:p-5\">\n <div className=\"flex gap-3\">\n <span className=\"flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-status-info-bg text-status-info-text\">\n <Info className=\"h-5 w-5\" />\n </span>\n <div className=\"min-w-0\">\n <h3 className=\"text-sm font-semibold text-text-primary\">\n How this is measured\n </h3>\n <p className=\"mt-1 text-sm text-text-secondary\">{radar.note}</p>\n </div>\n </div>\n </GlassCard>\n )}\n\n {noBuckets ? (\n <EmptyState\n title=\"No dead-time detected yet\"\n description=\"Once you log friction moments — time spent waiting, commuting, or on-hold — and leave gaps in your tracked day, your reclaimable hours will show up here.\"\n action={\n <span className=\"inline-flex items-center gap-2 rounded-md bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text\">\n <Radar className=\"h-4 w-4\" />\n Log your first friction moment\n </span>\n }\n />\n ) : (\n <>\n {/* Per-bucket breakdown cards. */}\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3\">\n {sortedBuckets.map((bucket) => {\n const BucketIcon = kindIcon(bucket.kind);\n return (\n <GlassCard\n key={bucket.kind}\n treatment=\"glass\"\n className=\"p-4 sm:p-5\"\n >\n <div className=\"flex items-start justify-between gap-3\">\n <div className=\"min-w-0\">\n <p className=\"truncate text-sm font-medium text-text-secondary\">\n {kindLabel(bucket.kind)}\n </p>\n <p className=\"mt-1.5 text-2xl font-bold tabular-nums text-text-primary\">\n {formatSecondsHuman(bucket.totalSeconds)}\n </p>\n </div>\n <span className=\"flex h-10 w-10 shrink-0 items-center justify-center rounded-lg bg-action-primary-bg/10 text-primary\">\n <BucketIcon className=\"h-5 w-5\" />\n </span>\n </div>\n <div className=\"mt-4\">\n <ProgressMeter\n value={bucket.reclaimableSeconds}\n max={bucket.totalSeconds || 1}\n label=\"Reclaimable\"\n tone=\"warning\"\n size=\"sm\"\n format={(v) => formatSecondsHuman(v)}\n />\n </div>\n </GlassCard>\n );\n })}\n </div>\n\n {/* Total vs reclaimable per friction source. */}\n <ChartCard\n title=\"Friction by source\"\n description=\"Total dead-time against the portion you could reclaim, per source\"\n >\n <BarsChart\n data={chartData}\n xKey=\"kind\"\n orientation=\"horizontal\"\n categoryWidth={140}\n series={[\n { key: \"total\", label: \"Total dead-time\" },\n { key: \"reclaimable\", label: \"Reclaimable\" },\n ]}\n valueFormat={(v) => formatSecondsHuman(v)}\n height={Math.max(220, sortedBuckets.length * 56)}\n emptyLabel=\"No friction sources in this range.\"\n ariaLabel=\"Total and reclaimable dead-time by source\"\n />\n </ChartCard>\n </>\n )}\n </div>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"FiniteLifePage.js","names":[],"sources":["../../../../src/timecampus/pages/lifetime/FiniteLifePage.tsx"],"sourcesContent":["import { useMemo, useState, type FormEvent } from \"react\";\nimport {\n Hourglass,\n Sparkles,\n SlidersHorizontal,\n CalendarDays,\n Lock,\n ArrowRight,\n} from \"lucide-react\";\nimport { GlassCard } from \"@burdenoff/fe-libs/ui\";\nimport {\n PageLayout,\n ChartCard,\n EmptyState,\n Modal,\n DonutChart,\n WeeksGrid,\n LoadingSkeleton,\n ToastContainer,\n useToast,\n type DonutSlice,\n} from \"../../components\";\nimport { useTimeCampus } from \"../../providers/TimeCampusProvider\";\nimport {\n useMyLifeInWeeksQuery,\n useMyLifeProfileQuery,\n useUpdateMyLifeProfileMutation,\n type MyLifeInWeeksQuery,\n} from \"@/generated/wspace-operations\";\nimport { lifeDomainLabel, formatSecondsHuman } from \"./domains\";\n\n/** One life-domain roll-up row, typed from the generated query result. */\ntype LifeDomainRollup =\n MyLifeInWeeksQuery[\"myLifeInWeeks\"][\"domainRollups\"][number];\n\n/** Grouped integer (e.g. 1,742). */\nfunction nf(n: number): string {\n return Math.max(0, Math.round(n)).toLocaleString();\n}\n\n/** Whole-week count → years with one decimal (e.g. 33.5). */\nfunction weeksToYears(weeks: number): string {\n return (Math.round((weeks / 52) * 10) / 10).toFixed(1);\n}\n\n/** ISO datetime → the `YYYY-MM-DD` a native date input expects. */\nfunction isoToDateInput(iso: string | null | undefined): string {\n if (!iso) return \"\";\n return iso.slice(0, 10);\n}\n\nconst TODAY_DATE_INPUT = new Date().toISOString().slice(0, 10);\n\n/**\n * FiniteLifePage — the finite-life \"your life in weeks\" memento.\n *\n * A calm, opt-in, entirely private reflection: each cell is one week of a life,\n * anchored to the gentle north star \"What if today was your last?\". Everything\n * it draws comes from the viewer's own (optional) birth date and life\n * expectancy — never a guessed age or an invented mortality date. It is\n * self-scoped (identity travels in the gateway headers, so the queries take no\n * user argument) and honest by construction: with no birth date it still\n * renders an open horizon and simply invites you to add one. It respects the\n * `hideRemainingWeeks` choice so no one is forced to stare down a countdown.\n */\nexport function FiniteLifePage() {\n const { workspaceId = \"\" } = useTimeCampus();\n const { toasts, addToast, dismissToast } = useToast();\n\n const {\n data: weeksData,\n loading: weeksLoading,\n error: weeksError,\n refetch: refetchWeeks,\n } = useMyLifeInWeeksQuery({ skip: !workspaceId });\n\n const {\n data: profileData,\n loading: profileLoading,\n error: profileError,\n refetch: refetchProfile,\n } = useMyLifeProfileQuery({ skip: !workspaceId });\n\n const [updateProfile, { loading: saving }] = useUpdateMyLifeProfileMutation();\n\n const weeks = weeksData?.myLifeInWeeks;\n const profile = profileData?.myLifeProfile;\n\n // ---- Setup modal (opt-in, dismissible) -----------------------------------\n const [setupOpen, setSetupOpen] = useState(false);\n const [birthDateInput, setBirthDateInput] = useState(\"\");\n const [expectancyInput, setExpectancyInput] = useState(\"\");\n const [hideRemainingInput, setHideRemainingInput] = useState(false);\n const [formError, setFormError] = useState<string | null>(null);\n\n const openSetup = () => {\n setBirthDateInput(isoToDateInput(profile?.birthDate ?? weeks?.birthDate));\n setExpectancyInput(\n profile?.lifeExpectancyYears != null\n ? String(profile.lifeExpectancyYears)\n : weeks?.lifeExpectancyYears != null\n ? String(weeks.lifeExpectancyYears)\n : \"\",\n );\n setHideRemainingInput(\n profile?.hideRemainingWeeks ?? weeks?.hideRemainingWeeks ?? false,\n );\n setFormError(null);\n setSetupOpen(true);\n };\n\n const handleSave = async (e?: FormEvent) => {\n e?.preventDefault();\n setFormError(null);\n\n if (!birthDateInput) {\n setFormError(\"Please add your birth date to continue.\");\n return;\n }\n if (birthDateInput > TODAY_DATE_INPUT) {\n setFormError(\"Your birth date can't be in the future.\");\n return;\n }\n\n let expectancy: number | null = null;\n if (expectancyInput.trim() !== \"\") {\n const parsed = Number(expectancyInput);\n if (!Number.isInteger(parsed) || parsed < 1 || parsed > 130) {\n setFormError(\"Life expectancy should be a whole number of years (1–130).\");\n return;\n }\n expectancy = parsed;\n }\n\n try {\n await updateProfile({\n variables: {\n input: {\n birthDate: new Date(`${birthDateInput}T00:00:00.000Z`).toISOString(),\n lifeExpectancyYears: expectancy,\n hideRemainingWeeks: hideRemainingInput,\n finiteLifeOptIn: true,\n },\n },\n });\n await Promise.all([refetchWeeks(), refetchProfile()]);\n setSetupOpen(false);\n addToast({\n variant: \"success\",\n title: \"Saved — this is just for you\",\n description: \"You can change or turn this off anytime.\",\n });\n } catch {\n addToast({\n variant: \"error\",\n title: \"Couldn't save your settings\",\n description: \"Please try again in a moment.\",\n });\n }\n };\n\n // ---- Derived, honest values ----------------------------------------------\n const hasBirthDate = !!weeks?.birthDate;\n const hideRemaining =\n weeks?.hideRemainingWeeks ?? profile?.hideRemainingWeeks ?? false;\n\n const weeksLived = weeks?.weeksLived ?? null;\n const expectancyYears = weeks?.lifeExpectancyYears ?? null;\n // Only reveal the full mortal horizon when the user hasn't asked to hide it.\n const gridTotalWeeks = hideRemaining ? null : (weeks?.expectedTotalWeeks ?? null);\n const weeksRemaining =\n hideRemaining || weeks?.weeksRemaining == null ? null : weeks.weeksRemaining;\n\n // Reflection: where the recent (backend-defined) window actually went.\n const domainSlices: DonutSlice[] = useMemo(() => {\n const rollups: LifeDomainRollup[] = weeks?.domainRollups ?? [];\n return rollups\n .filter((r) => r.totalSeconds > 0)\n .map((r) => ({\n name: lifeDomainLabel(r.domain),\n value: r.totalSeconds,\n }));\n }, [weeks?.domainRollups]);\n const rollupTotalSeconds = domainSlices.reduce((sum, s) => sum + s.value, 0);\n\n const loading = weeksLoading || profileLoading;\n const hardError = (weeksError || profileError) && !weeks && !profile;\n\n const setupAction = (\n <button\n type=\"button\"\n onClick={openSetup}\n className=\"inline-flex items-center gap-1.5 rounded-md border border-border-subtle bg-bg-surface px-4 py-2 text-sm font-medium text-text-primary hover:bg-bg-sunken\"\n >\n <SlidersHorizontal className=\"h-4 w-4\" />\n Life settings\n </button>\n );\n\n if (loading && !weeks && !profile) {\n return (\n <PageLayout\n title=\"Your Life in Weeks\"\n description=\"A calm, private view of the weeks you've lived — and the ones still ahead.\"\n >\n <LoadingSkeleton variant=\"detail\" />\n </PageLayout>\n );\n }\n\n return (\n <PageLayout\n title=\"Your Life in Weeks\"\n description=\"A calm, private view of the weeks you've lived — and the ones still ahead.\"\n purpose=\"Each square is a single week of your life. It's a gentle memento — not a countdown — meant only for you, to help this week feel a little more intentional.\"\n actions={setupAction}\n >\n {hardError ? (\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 We couldn't load your life view right now. Please try again in a moment.\n </div>\n ) : (\n <div className=\"space-y-6\">\n <div className=\"grid grid-cols-1 gap-6 lg:grid-cols-3\">\n {/* The grid — the memento itself */}\n <div className=\"lg:col-span-2\">\n <ChartCard\n title=\"Your life in weeks\"\n description={\n hasBirthDate\n ? \"Each row is a year; each square, a week you have lived or have yet to live.\"\n : \"Add your birth date to fill in the weeks you've already lived.\"\n }\n >\n <WeeksGrid\n weeksLived={weeksLived}\n totalWeeks={gridTotalWeeks}\n ariaLabel={\n hasBirthDate\n ? `Your life in weeks: ${nf(weeksLived ?? 0)} weeks lived so far`\n : \"Your life in weeks — birth date not set yet\"\n }\n />\n <div className=\"mt-4 flex flex-wrap items-center gap-x-5 gap-y-2 text-xs text-text-secondary\">\n <span className=\"flex items-center gap-1.5\">\n <span className=\"h-3 w-3 rounded-[3px] bg-action-primary-bg\" />\n Weeks lived\n </span>\n <span className=\"flex items-center gap-1.5\">\n <span className=\"h-3 w-3 rounded-[3px] border-2 border-action-primary-bg bg-bg-surface\" />\n This week\n </span>\n <span className=\"flex items-center gap-1.5\">\n <span className=\"h-3 w-3 rounded-[3px] border border-border-seam\" />\n Ahead\n </span>\n </div>\n </ChartCard>\n </div>\n\n {/* The calm side card */}\n <GlassCard treatment=\"glass\" glow className=\"flex flex-col p-5 sm:p-6\">\n <div className=\"flex h-10 w-10 items-center justify-center rounded-xl bg-action-primary-bg/10 text-primary\">\n <Hourglass className=\"h-5 w-5\" />\n </div>\n\n {hasBirthDate ? (\n <div className=\"mt-4 space-y-5\">\n <div>\n <p className=\"text-sm text-text-secondary\">You have lived</p>\n <p className=\"mt-0.5 text-3xl font-bold tabular-nums text-text-primary\">\n {nf(weeksLived ?? 0)}{\" \"}\n <span className=\"text-xl font-semibold text-text-secondary\">\n weeks\n </span>\n </p>\n </div>\n\n {expectancyYears != null && weeksRemaining != null ? (\n <div>\n <p className=\"text-sm text-text-secondary\">\n If you live to {expectancyYears}, about\n </p>\n <p className=\"mt-0.5 text-2xl font-bold tabular-nums text-text-primary\">\n {nf(weeksRemaining)}{\" \"}\n <span className=\"text-lg font-semibold text-text-secondary\">\n remain\n </span>\n </p>\n <p className=\"mt-1 text-xs text-text-secondary\">\n {weeksToYears(weeksLived ?? 0)} years lived ·{\" \"}\n {weeksToYears(weeksRemaining)} years ahead\n </p>\n </div>\n ) : (\n <p className=\"text-xs text-text-secondary\">\n {hideRemaining\n ? \"You've chosen to see only the weeks you've lived.\"\n : \"Add a life expectancy in settings to gently sketch the weeks ahead.\"}\n </p>\n )}\n\n <div className=\"rounded-lg border border-border-seam bg-bg-surface/40 p-3\">\n <p className=\"flex items-start gap-2 text-sm text-text-primary\">\n <Sparkles className=\"mt-0.5 h-4 w-4 shrink-0 text-primary\" />\n <span>\n What if today was your last? Make this week count.\n </span>\n </p>\n </div>\n </div>\n ) : (\n <div className=\"mt-4 space-y-4\">\n <div>\n <h3 className=\"text-base font-semibold text-text-primary\">\n See your life in weeks\n </h3>\n <p className=\"mt-1 text-sm text-text-secondary\">\n Add your birth date and we'll gently fill in the weeks\n you've lived so far — a quiet reminder that time is finite,\n and this week is yours to spend.\n </p>\n </div>\n <button\n type=\"button\"\n onClick={openSetup}\n className=\"inline-flex items-center gap-1.5 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 Add your birth date\n <ArrowRight className=\"h-4 w-4\" />\n </button>\n </div>\n )}\n\n <p className=\"mt-6 flex items-start gap-1.5 text-xs text-text-secondary\">\n <Lock className=\"mt-0.5 h-3.5 w-3.5 shrink-0\" />\n <span>\n Only you can see this. Your birth date is never shared, and you\n can turn this off anytime.\n </span>\n </p>\n </GlassCard>\n </div>\n\n {/* Gentle reflection — only when there's real recent data to show */}\n {domainSlices.length > 0 && (\n <ChartCard\n title=\"Where your recent weeks went\"\n description=\"How your logged time settled across life's domains lately — a mirror, not a scorecard.\"\n >\n <DonutChart\n data={domainSlices}\n centerLabel=\"logged lately\"\n centerValue={formatSecondsHuman(rollupTotalSeconds)}\n valueFormat={(v) => formatSecondsHuman(v)}\n emptyLabel=\"Nothing logged in this window yet.\"\n ariaLabel=\"Recent time by life domain\"\n />\n </ChartCard>\n )}\n\n {/* Honest first-run nudge when nothing has been logged either */}\n {hasBirthDate && domainSlices.length === 0 && (\n <EmptyState\n title=\"Your weeks are yours to shape\"\n description=\"As you capture how you spend your time, a quiet reflection of your recent weeks will appear here — only for you.\"\n />\n )}\n </div>\n )}\n\n {/* Opt-in setup */}\n <Modal\n open={setupOpen}\n onClose={() => setSetupOpen(false)}\n title=\"Your life in weeks\"\n description=\"This is private and entirely optional. Add only what feels comfortable.\"\n footer={\n <>\n <button\n type=\"button\"\n onClick={() => setSetupOpen(false)}\n className=\"rounded-md border border-border-subtle px-4 py-2 text-sm text-text-secondary hover:bg-bg-sunken hover:text-text-primary\"\n >\n Cancel\n </button>\n <button\n type=\"submit\"\n form=\"finite-life-setup\"\n disabled={saving}\n className=\"inline-flex items-center gap-1.5 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 {saving ? \"Saving…\" : \"Save\"}\n </button>\n </>\n }\n >\n <form id=\"finite-life-setup\" onSubmit={handleSave} className=\"space-y-5\">\n <div>\n <label\n htmlFor=\"finite-life-birthdate\"\n className=\"flex items-center gap-1.5 text-sm font-medium text-text-primary\"\n >\n <CalendarDays className=\"h-4 w-4 text-text-secondary\" />\n Your birth date\n </label>\n <input\n id=\"finite-life-birthdate\"\n type=\"date\"\n value={birthDateInput}\n max={TODAY_DATE_INPUT}\n onChange={(e) => setBirthDateInput(e.target.value)}\n className=\"mt-2 w-full rounded-lg border border-border-subtle bg-bg-sunken px-3 py-2 text-sm text-text-primary\"\n />\n <p className=\"mt-1.5 text-xs text-text-secondary\">\n Used only to count the weeks you've lived. It stays with you.\n </p>\n </div>\n\n <div>\n <label\n htmlFor=\"finite-life-expectancy\"\n className=\"text-sm font-medium text-text-primary\"\n >\n Life expectancy{\" \"}\n <span className=\"font-normal text-text-secondary\">(optional)</span>\n </label>\n <input\n id=\"finite-life-expectancy\"\n type=\"number\"\n inputMode=\"numeric\"\n min={1}\n max={130}\n step={1}\n placeholder=\"e.g. 90\"\n value={expectancyInput}\n onChange={(e) => setExpectancyInput(e.target.value)}\n className=\"mt-2 w-full rounded-lg border border-border-subtle bg-bg-sunken px-3 py-2 text-sm text-text-primary\"\n />\n <p className=\"mt-1.5 text-xs text-text-secondary\">\n A gentle horizon for the weeks ahead — never a deadline. Leave it\n blank to simply see the weeks you've lived.\n </p>\n </div>\n\n <label className=\"flex items-start gap-2.5 rounded-lg border border-border-subtle bg-bg-surface/40 p-3\">\n <input\n type=\"checkbox\"\n checked={hideRemainingInput}\n onChange={(e) => setHideRemainingInput(e.target.checked)}\n className=\"mt-0.5 h-4 w-4 shrink-0 rounded border-border-subtle\"\n />\n <span className=\"text-sm text-text-primary\">\n Hide the weeks ahead\n <span className=\"mt-0.5 block text-xs text-text-secondary\">\n Show only the weeks you've lived — no horizon, no countdown.\n </span>\n </span>\n </label>\n\n {formError && (\n <p className=\"rounded-lg border border-status-error-border/50 bg-status-error-bg/10 px-3 py-2 text-sm text-status-error-text\">\n {formError}\n </p>\n )}\n </form>\n </Modal>\n\n <ToastContainer toasts={toasts} onDismiss={dismissToast} />\n </PageLayout>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"FiniteLifePage.js","names":[],"sources":["../../../../src/timecampus/pages/lifetime/FiniteLifePage.tsx"],"sourcesContent":["import { useMemo, useState, type FormEvent } from \"react\";\nimport {\n Hourglass,\n Sparkles,\n SlidersHorizontal,\n CalendarDays,\n Lock,\n ArrowRight,\n} from \"lucide-react\";\nimport { GlassCard } from \"@burdenoff/fe-libs/ui\";\nimport {\n PageLayout,\n ChartCard,\n EmptyState,\n Modal,\n DonutChart,\n WeeksGrid,\n LoadingSkeleton,\n ToastContainer,\n useToast,\n type DonutSlice,\n} from \"../../components\";\nimport { useTimeCampus } from \"../../providers/TimeCampusProvider\";\nimport {\n useMyLifeInWeeksQuery,\n useMyLifeProfileQuery,\n useUpdateMyLifeProfileMutation,\n type MyLifeInWeeksQuery,\n} from \"@/generated/wspace-operations\";\nimport { lifeDomainLabel, formatSecondsHuman } from \"./domains\";\n\n/** One life-domain roll-up row, typed from the generated query result. */\ntype LifeDomainRollup =\n MyLifeInWeeksQuery[\"myLifeInWeeks\"][\"domainRollups\"][number];\n\n/** Grouped integer (e.g. 1,742). */\nfunction nf(n: number): string {\n return Math.max(0, Math.round(n)).toLocaleString();\n}\n\n/** Whole-week count → years with one decimal (e.g. 33.5). */\nfunction weeksToYears(weeks: number): string {\n return (Math.round((weeks / 52) * 10) / 10).toFixed(1);\n}\n\n/** ISO datetime → the `YYYY-MM-DD` a native date input expects. */\nfunction isoToDateInput(iso: string | null | undefined): string {\n if (!iso) return \"\";\n return iso.slice(0, 10);\n}\n\nconst TODAY_DATE_INPUT = new Date().toISOString().slice(0, 10);\n\n/**\n * FiniteLifePage — the finite-life \"your life in weeks\" memento.\n *\n * A calm, opt-in, entirely private reflection: each cell is one week of a life,\n * anchored to the gentle north star \"What if today was your last?\". Everything\n * it draws comes from the viewer's own (optional) birth date and life\n * expectancy — never a guessed age or an invented mortality date. It is\n * self-scoped (identity travels in the gateway headers, so the queries take no\n * user argument) and honest by construction: with no birth date it still\n * renders an open horizon and simply invites you to add one. It respects the\n * `hideRemainingWeeks` choice so no one is forced to stare down a countdown.\n */\nexport function FiniteLifePage() {\n const { workspaceId = \"\" } = useTimeCampus();\n const { toasts, addToast, dismissToast } = useToast();\n\n const {\n data: weeksData,\n loading: weeksLoading,\n error: weeksError,\n refetch: refetchWeeks,\n } = useMyLifeInWeeksQuery({ skip: !workspaceId });\n\n const {\n data: profileData,\n loading: profileLoading,\n error: profileError,\n refetch: refetchProfile,\n } = useMyLifeProfileQuery({ skip: !workspaceId });\n\n const [updateProfile, { loading: saving }] = useUpdateMyLifeProfileMutation();\n\n const weeks = weeksData?.myLifeInWeeks;\n const profile = profileData?.myLifeProfile;\n\n // ---- Setup modal (opt-in, dismissible) -----------------------------------\n const [setupOpen, setSetupOpen] = useState(false);\n const [birthDateInput, setBirthDateInput] = useState(\"\");\n const [expectancyInput, setExpectancyInput] = useState(\"\");\n const [hideRemainingInput, setHideRemainingInput] = useState(false);\n const [formError, setFormError] = useState<string | null>(null);\n\n const openSetup = () => {\n setBirthDateInput(isoToDateInput(profile?.birthDate ?? weeks?.birthDate));\n setExpectancyInput(\n profile?.lifeExpectancyYears != null\n ? String(profile.lifeExpectancyYears)\n : weeks?.lifeExpectancyYears != null\n ? String(weeks.lifeExpectancyYears)\n : \"\",\n );\n setHideRemainingInput(\n profile?.hideRemainingWeeks ?? weeks?.hideRemainingWeeks ?? false,\n );\n setFormError(null);\n setSetupOpen(true);\n };\n\n const handleSave = async (e?: FormEvent) => {\n e?.preventDefault();\n setFormError(null);\n\n if (!birthDateInput) {\n setFormError(\"Please add your birth date to continue.\");\n return;\n }\n if (birthDateInput > TODAY_DATE_INPUT) {\n setFormError(\"Your birth date can't be in the future.\");\n return;\n }\n\n let expectancy: number | null = null;\n if (expectancyInput.trim() !== \"\") {\n const parsed = Number(expectancyInput);\n if (!Number.isInteger(parsed) || parsed < 1 || parsed > 130) {\n setFormError(\n \"Life expectancy should be a whole number of years (1–130).\",\n );\n return;\n }\n expectancy = parsed;\n }\n\n try {\n await updateProfile({\n variables: {\n input: {\n birthDate: new Date(\n `${birthDateInput}T00:00:00.000Z`,\n ).toISOString(),\n lifeExpectancyYears: expectancy,\n hideRemainingWeeks: hideRemainingInput,\n finiteLifeOptIn: true,\n },\n },\n });\n await Promise.all([refetchWeeks(), refetchProfile()]);\n setSetupOpen(false);\n addToast({\n variant: \"success\",\n title: \"Saved — this is just for you\",\n description: \"You can change or turn this off anytime.\",\n });\n } catch {\n addToast({\n variant: \"error\",\n title: \"Couldn't save your settings\",\n description: \"Please try again in a moment.\",\n });\n }\n };\n\n // ---- Derived, honest values ----------------------------------------------\n const hasBirthDate = !!weeks?.birthDate;\n const hideRemaining =\n weeks?.hideRemainingWeeks ?? profile?.hideRemainingWeeks ?? false;\n\n const weeksLived = weeks?.weeksLived ?? null;\n const expectancyYears = weeks?.lifeExpectancyYears ?? null;\n // Only reveal the full mortal horizon when the user hasn't asked to hide it.\n const gridTotalWeeks = hideRemaining\n ? null\n : (weeks?.expectedTotalWeeks ?? null);\n const weeksRemaining =\n hideRemaining || weeks?.weeksRemaining == null\n ? null\n : weeks.weeksRemaining;\n\n // Reflection: where the recent (backend-defined) window actually went.\n const domainSlices: DonutSlice[] = useMemo(() => {\n const rollups: LifeDomainRollup[] = weeks?.domainRollups ?? [];\n return rollups\n .filter((r) => r.totalSeconds > 0)\n .map((r) => ({\n name: lifeDomainLabel(r.domain),\n value: r.totalSeconds,\n }));\n }, [weeks?.domainRollups]);\n const rollupTotalSeconds = domainSlices.reduce((sum, s) => sum + s.value, 0);\n\n const loading = weeksLoading || profileLoading;\n const hardError = (weeksError || profileError) && !weeks && !profile;\n\n const setupAction = (\n <button\n type=\"button\"\n onClick={openSetup}\n className=\"inline-flex items-center gap-1.5 rounded-md border border-border-subtle bg-bg-surface px-4 py-2 text-sm font-medium text-text-primary hover:bg-bg-sunken\"\n >\n <SlidersHorizontal className=\"h-4 w-4\" />\n Life settings\n </button>\n );\n\n if (loading && !weeks && !profile) {\n return (\n <PageLayout\n title=\"Your Life in Weeks\"\n description=\"A calm, private view of the weeks you've lived — and the ones still ahead.\"\n >\n <LoadingSkeleton variant=\"detail\" />\n </PageLayout>\n );\n }\n\n return (\n <PageLayout\n title=\"Your Life in Weeks\"\n description=\"A calm, private view of the weeks you've lived — and the ones still ahead.\"\n purpose=\"Each square is a single week of your life. It's a gentle memento — not a countdown — meant only for you, to help this week feel a little more intentional.\"\n actions={setupAction}\n >\n {hardError ? (\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 We couldn't load your life view right now. Please try again in a\n moment.\n </div>\n ) : (\n <div className=\"space-y-6\">\n <div className=\"grid grid-cols-1 gap-6 lg:grid-cols-3\">\n {/* The grid — the memento itself */}\n <div className=\"lg:col-span-2\">\n <ChartCard\n title=\"Your life in weeks\"\n description={\n hasBirthDate\n ? \"Each row is a year; each square, a week you have lived or have yet to live.\"\n : \"Add your birth date to fill in the weeks you've already lived.\"\n }\n >\n <WeeksGrid\n weeksLived={weeksLived}\n totalWeeks={gridTotalWeeks}\n ariaLabel={\n hasBirthDate\n ? `Your life in weeks: ${nf(weeksLived ?? 0)} weeks lived so far`\n : \"Your life in weeks — birth date not set yet\"\n }\n />\n <div className=\"mt-4 flex flex-wrap items-center gap-x-5 gap-y-2 text-xs text-text-secondary\">\n <span className=\"flex items-center gap-1.5\">\n <span className=\"h-3 w-3 rounded-[3px] bg-action-primary-bg\" />\n Weeks lived\n </span>\n <span className=\"flex items-center gap-1.5\">\n <span className=\"h-3 w-3 rounded-[3px] border-2 border-action-primary-bg bg-bg-surface\" />\n This week\n </span>\n <span className=\"flex items-center gap-1.5\">\n <span className=\"h-3 w-3 rounded-[3px] border border-border-seam\" />\n Ahead\n </span>\n </div>\n </ChartCard>\n </div>\n\n {/* The calm side card */}\n <GlassCard\n treatment=\"glass\"\n glow\n className=\"flex flex-col p-5 sm:p-6\"\n >\n <div className=\"flex h-10 w-10 items-center justify-center rounded-xl bg-action-primary-bg/10 text-primary\">\n <Hourglass className=\"h-5 w-5\" />\n </div>\n\n {hasBirthDate ? (\n <div className=\"mt-4 space-y-5\">\n <div>\n <p className=\"text-sm text-text-secondary\">\n You have lived\n </p>\n <p className=\"mt-0.5 text-3xl font-bold tabular-nums text-text-primary\">\n {nf(weeksLived ?? 0)}{\" \"}\n <span className=\"text-xl font-semibold text-text-secondary\">\n weeks\n </span>\n </p>\n </div>\n\n {expectancyYears != null && weeksRemaining != null ? (\n <div>\n <p className=\"text-sm text-text-secondary\">\n If you live to {expectancyYears}, about\n </p>\n <p className=\"mt-0.5 text-2xl font-bold tabular-nums text-text-primary\">\n {nf(weeksRemaining)}{\" \"}\n <span className=\"text-lg font-semibold text-text-secondary\">\n remain\n </span>\n </p>\n <p className=\"mt-1 text-xs text-text-secondary\">\n {weeksToYears(weeksLived ?? 0)} years lived ·{\" \"}\n {weeksToYears(weeksRemaining)} years ahead\n </p>\n </div>\n ) : (\n <p className=\"text-xs text-text-secondary\">\n {hideRemaining\n ? \"You've chosen to see only the weeks you've lived.\"\n : \"Add a life expectancy in settings to gently sketch the weeks ahead.\"}\n </p>\n )}\n\n <div className=\"rounded-lg border border-border-seam bg-bg-surface/40 p-3\">\n <p className=\"flex items-start gap-2 text-sm text-text-primary\">\n <Sparkles className=\"mt-0.5 h-4 w-4 shrink-0 text-primary\" />\n <span>\n What if today was your last? Make this week count.\n </span>\n </p>\n </div>\n </div>\n ) : (\n <div className=\"mt-4 space-y-4\">\n <div>\n <h3 className=\"text-base font-semibold text-text-primary\">\n See your life in weeks\n </h3>\n <p className=\"mt-1 text-sm text-text-secondary\">\n Add your birth date and we'll gently fill in the weeks\n you've lived so far — a quiet reminder that time is\n finite, and this week is yours to spend.\n </p>\n </div>\n <button\n type=\"button\"\n onClick={openSetup}\n className=\"inline-flex items-center gap-1.5 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 Add your birth date\n <ArrowRight className=\"h-4 w-4\" />\n </button>\n </div>\n )}\n\n <p className=\"mt-6 flex items-start gap-1.5 text-xs text-text-secondary\">\n <Lock className=\"mt-0.5 h-3.5 w-3.5 shrink-0\" />\n <span>\n Only you can see this. Your birth date is never shared, and\n you can turn this off anytime.\n </span>\n </p>\n </GlassCard>\n </div>\n\n {/* Gentle reflection — only when there's real recent data to show */}\n {domainSlices.length > 0 && (\n <ChartCard\n title=\"Where your recent weeks went\"\n description=\"How your logged time settled across life's domains lately — a mirror, not a scorecard.\"\n >\n <DonutChart\n data={domainSlices}\n centerLabel=\"logged lately\"\n centerValue={formatSecondsHuman(rollupTotalSeconds)}\n valueFormat={(v) => formatSecondsHuman(v)}\n emptyLabel=\"Nothing logged in this window yet.\"\n ariaLabel=\"Recent time by life domain\"\n />\n </ChartCard>\n )}\n\n {/* Honest first-run nudge when nothing has been logged either */}\n {hasBirthDate && domainSlices.length === 0 && (\n <EmptyState\n title=\"Your weeks are yours to shape\"\n description=\"As you capture how you spend your time, a quiet reflection of your recent weeks will appear here — only for you.\"\n />\n )}\n </div>\n )}\n\n {/* Opt-in setup */}\n <Modal\n open={setupOpen}\n onClose={() => setSetupOpen(false)}\n title=\"Your life in weeks\"\n description=\"This is private and entirely optional. Add only what feels comfortable.\"\n footer={\n <>\n <button\n type=\"button\"\n onClick={() => setSetupOpen(false)}\n className=\"rounded-md border border-border-subtle px-4 py-2 text-sm text-text-secondary hover:bg-bg-sunken hover:text-text-primary\"\n >\n Cancel\n </button>\n <button\n type=\"submit\"\n form=\"finite-life-setup\"\n disabled={saving}\n className=\"inline-flex items-center gap-1.5 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 {saving ? \"Saving…\" : \"Save\"}\n </button>\n </>\n }\n >\n <form\n id=\"finite-life-setup\"\n onSubmit={handleSave}\n className=\"space-y-5\"\n >\n <div>\n <label\n htmlFor=\"finite-life-birthdate\"\n className=\"flex items-center gap-1.5 text-sm font-medium text-text-primary\"\n >\n <CalendarDays className=\"h-4 w-4 text-text-secondary\" />\n Your birth date\n </label>\n <input\n id=\"finite-life-birthdate\"\n type=\"date\"\n value={birthDateInput}\n max={TODAY_DATE_INPUT}\n onChange={(e) => setBirthDateInput(e.target.value)}\n className=\"mt-2 w-full rounded-lg border border-border-subtle bg-bg-sunken px-3 py-2 text-sm text-text-primary\"\n />\n <p className=\"mt-1.5 text-xs text-text-secondary\">\n Used only to count the weeks you've lived. It stays with you.\n </p>\n </div>\n\n <div>\n <label\n htmlFor=\"finite-life-expectancy\"\n className=\"text-sm font-medium text-text-primary\"\n >\n Life expectancy{\" \"}\n <span className=\"font-normal text-text-secondary\">\n (optional)\n </span>\n </label>\n <input\n id=\"finite-life-expectancy\"\n type=\"number\"\n inputMode=\"numeric\"\n min={1}\n max={130}\n step={1}\n placeholder=\"e.g. 90\"\n value={expectancyInput}\n onChange={(e) => setExpectancyInput(e.target.value)}\n className=\"mt-2 w-full rounded-lg border border-border-subtle bg-bg-sunken px-3 py-2 text-sm text-text-primary\"\n />\n <p className=\"mt-1.5 text-xs text-text-secondary\">\n A gentle horizon for the weeks ahead — never a deadline. Leave it\n blank to simply see the weeks you've lived.\n </p>\n </div>\n\n <label className=\"flex items-start gap-2.5 rounded-lg border border-border-subtle bg-bg-surface/40 p-3\">\n <input\n type=\"checkbox\"\n checked={hideRemainingInput}\n onChange={(e) => setHideRemainingInput(e.target.checked)}\n className=\"mt-0.5 h-4 w-4 shrink-0 rounded border-border-subtle\"\n />\n <span className=\"text-sm text-text-primary\">\n Hide the weeks ahead\n <span className=\"mt-0.5 block text-xs text-text-secondary\">\n Show only the weeks you've lived — no horizon, no countdown.\n </span>\n </span>\n </label>\n\n {formError && (\n <p className=\"rounded-lg border border-status-error-border/50 bg-status-error-bg/10 px-3 py-2 text-sm text-status-error-text\">\n {formError}\n </p>\n )}\n </form>\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":"domains.js","names":[],"sources":["../../../../src/timecampus/pages/lifetime/domains.ts"],"sourcesContent":["import { TimeCampusLifeDomain } from \"@/generated/wspace-types\";\n\n/**\n * Human labels for the life-domain enum, shared by every life-time page so the\n * vocabulary stays consistent. The enum values themselves are the source of\n * truth (stored as category strings on the backend); this only maps them to\n * display text.\n */\nexport const LIFE_DOMAIN_LABEL: Record<TimeCampusLifeDomain, string> = {\n [TimeCampusLifeDomain.Work]: \"Work\",\n [TimeCampusLifeDomain.Sleep]: \"Sleep\",\n [TimeCampusLifeDomain.Health]: \"Health\",\n [TimeCampusLifeDomain.Family]: \"Family & relationships\",\n [TimeCampusLifeDomain.Learning]: \"Learning\",\n [TimeCampusLifeDomain.Leisure]: \"Leisure\",\n [TimeCampusLifeDomain.Commute]: \"Commute\",\n [TimeCampusLifeDomain.Waiting]: \"Waiting\",\n [TimeCampusLifeDomain.Chores]: \"Chores\",\n [TimeCampusLifeDomain.SelfCare]: \"Self-care\",\n [TimeCampusLifeDomain.Social]: \"Social\",\n [TimeCampusLifeDomain.FinanceAdmin]: \"Finance & admin\",\n [TimeCampusLifeDomain.Reflection]: \"Reflection\",\n [TimeCampusLifeDomain.Other]: \"Other\",\n};\n\n/** Domains the user typically logs by hand (excludes WORK, which is captured). */\nexport const PERSONAL_LIFE_DOMAINS: TimeCampusLifeDomain[] = [\n TimeCampusLifeDomain.Sleep,\n TimeCampusLifeDomain.Health,\n TimeCampusLifeDomain.Family,\n TimeCampusLifeDomain.Learning,\n TimeCampusLifeDomain.Leisure,\n TimeCampusLifeDomain.Commute,\n TimeCampusLifeDomain.Waiting,\n TimeCampusLifeDomain.Chores,\n TimeCampusLifeDomain.SelfCare,\n TimeCampusLifeDomain.Social,\n TimeCampusLifeDomain.FinanceAdmin,\n TimeCampusLifeDomain.Reflection,\n];\n\n/** All life domains in a sensible display order. */\nexport const ALL_LIFE_DOMAINS: TimeCampusLifeDomain[] = [\n TimeCampusLifeDomain.Work,\n ...PERSONAL_LIFE_DOMAINS,\n TimeCampusLifeDomain.Other,\n];\n\nexport function lifeDomainLabel(domain: TimeCampusLifeDomain | string): string {\n return (\n LIFE_DOMAIN_LABEL[domain as TimeCampusLifeDomain] ?? String(domain)\n );\n}\n\n/** Whole hours from seconds, one decimal (e.g. 6.5). */\nexport function secondsToHours(seconds: number): number {\n return Math.round((seconds / 3600) * 10) / 10;\n}\n\n/** A compact \"6h 30m\" from seconds. */\nexport function formatSecondsHuman(seconds: number): string {\n const s = Math.max(0, Math.round(seconds));\n const h = Math.floor(s / 3600);\n const m = Math.floor((s % 3600) / 60);\n if (h === 0 && m === 0) return \"0m\";\n if (h === 0) return `${m}m`;\n if (m === 0) return `${h}h`;\n return `${h}h ${m}m`;\n}\n\n/** ISO string for a date `days` ago at the start of the day. */\nexport function isoDaysAgo(days: number): string {\n const d = new Date(Date.now() - days * 86_400_000);\n d.setHours(0, 0, 0, 0);\n return d.toISOString();\n}\n\n/** ISO string for the end of today. */\nexport function isoNow(): string {\n return new Date().toISOString();\n}\n"],"mappings":";;AAQA,IAAa,IAA0D;EACpE,EAAqB,OAAO;EAC5B,EAAqB,QAAQ;EAC7B,EAAqB,SAAS;EAC9B,EAAqB,SAAS;EAC9B,EAAqB,WAAW;EAChC,EAAqB,UAAU;EAC/B,EAAqB,UAAU;EAC/B,EAAqB,UAAU;EAC/B,EAAqB,SAAS;EAC9B,EAAqB,WAAW;EAChC,EAAqB,SAAS;EAC9B,EAAqB,eAAe;EACpC,EAAqB,aAAa;EAClC,EAAqB,QAAQ;CAC/B,EAGY,IAAgD;CAC3D,EAAqB;CACrB,EAAqB;CACrB,EAAqB;CACrB,EAAqB;CACrB,EAAqB;CACrB,EAAqB;CACrB,EAAqB;CACrB,EAAqB;CACrB,EAAqB;CACrB,EAAqB;CACrB,EAAqB;CACrB,EAAqB;CACtB,EAGY,IAA2C;CACtD,EAAqB;CACrB,GAAG;CACH,EAAqB;CACtB;AAED,SAAgB,EAAgB,GAA+C;AAC7E,QACE,EAAkB,MAAmC,OAAO,EAAO;;AAKvE,SAAgB,EAAe,GAAyB;AACtD,QAAO,KAAK,MAAO,IAAU,OAAQ,GAAG,GAAG;;AAI7C,SAAgB,EAAmB,GAAyB;CAC1D,IAAM,IAAI,KAAK,IAAI,GAAG,KAAK,MAAM,EAAQ,CAAC,EACpC,IAAI,KAAK,MAAM,IAAI,KAAK,EACxB,IAAI,KAAK,MAAO,IAAI,OAAQ,GAAG;AAIrC,QAHI,MAAM,KAAK,MAAM,IAAU,OAC3B,MAAM,IAAU,GAAG,EAAE,KACrB,MAAM,IAAU,GAAG,EAAE,KAClB,GAAG,EAAE,IAAI,EAAE;;AAIpB,SAAgB,EAAW,GAAsB;CAC/C,IAAM,oBAAI,IAAI,KAAK,KAAK,KAAK,GAAG,IAAO,MAAW;AAElD,QADA,EAAE,SAAS,GAAG,GAAG,GAAG,EAAE,EACf,EAAE,aAAa;;AAIxB,SAAgB,IAAiB;AAC/B,yBAAO,IAAI,MAAM,EAAC,aAAa"}
1
+ {"version":3,"file":"domains.js","names":[],"sources":["../../../../src/timecampus/pages/lifetime/domains.ts"],"sourcesContent":["import { TimeCampusLifeDomain } from \"@/generated/wspace-types\";\n\n/**\n * Human labels for the life-domain enum, shared by every life-time page so the\n * vocabulary stays consistent. The enum values themselves are the source of\n * truth (stored as category strings on the backend); this only maps them to\n * display text.\n */\nexport const LIFE_DOMAIN_LABEL: Record<TimeCampusLifeDomain, string> = {\n [TimeCampusLifeDomain.Work]: \"Work\",\n [TimeCampusLifeDomain.Sleep]: \"Sleep\",\n [TimeCampusLifeDomain.Health]: \"Health\",\n [TimeCampusLifeDomain.Family]: \"Family & relationships\",\n [TimeCampusLifeDomain.Learning]: \"Learning\",\n [TimeCampusLifeDomain.Leisure]: \"Leisure\",\n [TimeCampusLifeDomain.Commute]: \"Commute\",\n [TimeCampusLifeDomain.Waiting]: \"Waiting\",\n [TimeCampusLifeDomain.Chores]: \"Chores\",\n [TimeCampusLifeDomain.SelfCare]: \"Self-care\",\n [TimeCampusLifeDomain.Social]: \"Social\",\n [TimeCampusLifeDomain.FinanceAdmin]: \"Finance & admin\",\n [TimeCampusLifeDomain.Reflection]: \"Reflection\",\n [TimeCampusLifeDomain.Other]: \"Other\",\n};\n\n/** Domains the user typically logs by hand (excludes WORK, which is captured). */\nexport const PERSONAL_LIFE_DOMAINS: TimeCampusLifeDomain[] = [\n TimeCampusLifeDomain.Sleep,\n TimeCampusLifeDomain.Health,\n TimeCampusLifeDomain.Family,\n TimeCampusLifeDomain.Learning,\n TimeCampusLifeDomain.Leisure,\n TimeCampusLifeDomain.Commute,\n TimeCampusLifeDomain.Waiting,\n TimeCampusLifeDomain.Chores,\n TimeCampusLifeDomain.SelfCare,\n TimeCampusLifeDomain.Social,\n TimeCampusLifeDomain.FinanceAdmin,\n TimeCampusLifeDomain.Reflection,\n];\n\n/** All life domains in a sensible display order. */\nexport const ALL_LIFE_DOMAINS: TimeCampusLifeDomain[] = [\n TimeCampusLifeDomain.Work,\n ...PERSONAL_LIFE_DOMAINS,\n TimeCampusLifeDomain.Other,\n];\n\nexport function lifeDomainLabel(domain: TimeCampusLifeDomain | string): string {\n return LIFE_DOMAIN_LABEL[domain as TimeCampusLifeDomain] ?? String(domain);\n}\n\n/** Whole hours from seconds, one decimal (e.g. 6.5). */\nexport function secondsToHours(seconds: number): number {\n return Math.round((seconds / 3600) * 10) / 10;\n}\n\n/** A compact \"6h 30m\" from seconds. */\nexport function formatSecondsHuman(seconds: number): string {\n const s = Math.max(0, Math.round(seconds));\n const h = Math.floor(s / 3600);\n const m = Math.floor((s % 3600) / 60);\n if (h === 0 && m === 0) return \"0m\";\n if (h === 0) return `${m}m`;\n if (m === 0) return `${h}h`;\n return `${h}h ${m}m`;\n}\n\n/** ISO string for a date `days` ago at the start of the day. */\nexport function isoDaysAgo(days: number): string {\n const d = new Date(Date.now() - days * 86_400_000);\n d.setHours(0, 0, 0, 0);\n return d.toISOString();\n}\n\n/** ISO string for the end of today. */\nexport function isoNow(): string {\n return new Date().toISOString();\n}\n"],"mappings":";;AAQA,IAAa,IAA0D;EACpE,EAAqB,OAAO;EAC5B,EAAqB,QAAQ;EAC7B,EAAqB,SAAS;EAC9B,EAAqB,SAAS;EAC9B,EAAqB,WAAW;EAChC,EAAqB,UAAU;EAC/B,EAAqB,UAAU;EAC/B,EAAqB,UAAU;EAC/B,EAAqB,SAAS;EAC9B,EAAqB,WAAW;EAChC,EAAqB,SAAS;EAC9B,EAAqB,eAAe;EACpC,EAAqB,aAAa;EAClC,EAAqB,QAAQ;CAC/B,EAGY,IAAgD;CAC3D,EAAqB;CACrB,EAAqB;CACrB,EAAqB;CACrB,EAAqB;CACrB,EAAqB;CACrB,EAAqB;CACrB,EAAqB;CACrB,EAAqB;CACrB,EAAqB;CACrB,EAAqB;CACrB,EAAqB;CACrB,EAAqB;CACtB,EAGY,IAA2C;CACtD,EAAqB;CACrB,GAAG;CACH,EAAqB;CACtB;AAED,SAAgB,EAAgB,GAA+C;AAC7E,QAAO,EAAkB,MAAmC,OAAO,EAAO;;AAI5E,SAAgB,EAAe,GAAyB;AACtD,QAAO,KAAK,MAAO,IAAU,OAAQ,GAAG,GAAG;;AAI7C,SAAgB,EAAmB,GAAyB;CAC1D,IAAM,IAAI,KAAK,IAAI,GAAG,KAAK,MAAM,EAAQ,CAAC,EACpC,IAAI,KAAK,MAAM,IAAI,KAAK,EACxB,IAAI,KAAK,MAAO,IAAI,OAAQ,GAAG;AAIrC,QAHI,MAAM,KAAK,MAAM,IAAU,OAC3B,MAAM,IAAU,GAAG,EAAE,KACrB,MAAM,IAAU,GAAG,EAAE,KAClB,GAAG,EAAE,IAAI,EAAE;;AAIpB,SAAgB,EAAW,GAAsB;CAC/C,IAAM,oBAAI,IAAI,KAAK,KAAK,KAAK,GAAG,IAAO,MAAW;AAElD,QADA,EAAE,SAAS,GAAG,GAAG,GAAG,EAAE,EACf,EAAE,aAAa;;AAIxB,SAAgB,IAAiB;AAC/B,yBAAO,IAAI,MAAM,EAAC,aAAa"}
@@ -1 +1 @@
1
- {"version":3,"file":"MeetingFormModal.js","names":[],"sources":["../../../../src/timecampus/pages/meetings/MeetingFormModal.tsx"],"sourcesContent":["import { useState, type FC } from \"react\";\nimport { gql } from \"@apollo/client\";\nimport { useApolloClient } from \"@apollo/client/react\";\nimport { uploadBlobToFiles } from \"@burdenoff/fe-libs/storage\";\nimport { Modal } from \"../../components/Modal\";\nimport {\n MeetingType,\n type CreateMeetingInput,\n type UpdateMeetingInput,\n} from \"@/generated/wspace-types\";\nimport type { MeetingFieldsFragment } from \"@/generated/wspace-operations\";\n\n/**\n * Resolve a downloadable URL for a Files object. The URL is presigned and\n * expires after 7 days (604800s) — a known limitation: a stored recordingUrl\n * will eventually 403 and the recording must be re-uploaded to refresh it.\n */\nconst GET_FILE_DOWNLOAD_URL = gql`\n query TimeCampus_GetFileDownloadUrl($id: ID!, $expiresIn: Int) {\n getFileDownloadUrl(id: $id, expiresIn: $expiresIn)\n }\n`;\n\n// wspace-files requires a real folder id (File.folderId FK).\nconst ENSURE_FOLDER = gql`\n mutation TimeCampusMeeting_EnsureFolder($path: String!) {\n ensureFolderByPath(path: $path) {\n id\n }\n }\n`;\n\nconst MEETING_TYPES: { value: MeetingType; label: string }[] = [\n { value: MeetingType.Team, label: \"Team\" },\n { value: MeetingType.Standup, label: \"Standup\" },\n { value: MeetingType.OneOnOne, label: \"One-on-one\" },\n { value: MeetingType.Planning, label: \"Planning\" },\n { value: MeetingType.Review, label: \"Review\" },\n { value: MeetingType.Retrospective, label: \"Retrospective\" },\n { value: MeetingType.External, label: \"External\" },\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\";\nconst labelClass = \"block text-sm font-medium text-text-primary\";\n\n/** Convert an ISO string into a value usable by a datetime-local input. */\nfunction toLocalInput(iso?: string | null): string {\n if (!iso) return \"\";\n const d = new Date(iso);\n if (Number.isNaN(d.getTime())) return \"\";\n const pad = (n: number) => String(n).padStart(2, \"0\");\n return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(\n d.getHours(),\n )}:${pad(d.getMinutes())}`;\n}\n\nexport interface MeetingFormValues {\n title: string;\n meetingType: MeetingType;\n startTime: string;\n endTime?: string;\n notes?: string;\n recordingUrl?: string;\n cost?: number;\n}\n\ninterface MeetingFormModalProps {\n open: boolean;\n onClose: () => void;\n /** Existing meeting when editing; undefined when creating. */\n meeting?: MeetingFieldsFragment;\n workspaceId: string;\n submitting: boolean;\n error?: boolean;\n /** Receives a ready-to-send input; caller wires it to the right mutation. */\n onCreate?: (input: CreateMeetingInput) => Promise<void> | void;\n onUpdate?: (input: UpdateMeetingInput) => Promise<void> | void;\n}\n\nexport const MeetingFormModal: FC<MeetingFormModalProps> = ({\n open,\n onClose,\n meeting,\n workspaceId,\n submitting,\n error,\n onCreate,\n onUpdate,\n}) => {\n const isEdit = Boolean(meeting);\n const [title, setTitle] = useState(meeting?.title ?? \"\");\n const [meetingType, setMeetingType] = useState<MeetingType>(\n (meeting?.meetingType as MeetingType | undefined) ?? MeetingType.Team,\n );\n const [start, setStart] = useState(toLocalInput(meeting?.startTime));\n const [duration, setDuration] = useState<number>(meeting?.duration ?? 30);\n const [notes, setNotes] = useState(meeting?.notes ?? \"\");\n const [recordingUrl, setRecordingUrl] = useState(meeting?.recordingUrl ?? \"\");\n const apollo = useApolloClient();\n const [recordingUploading, setRecordingUploading] = useState(false);\n const [recordingError, setRecordingError] = useState<string | null>(null);\n\n const canSubmit = Boolean(title.trim() && start) && !submitting;\n\n /**\n * Upload a recording (audio/video) to platform Files and store its presigned\n * URL in the existing recordingUrl state. Failures surface an inline error\n * but never block submitting the meeting — the recording is optional and the\n * manual URL field remains available as a fallback.\n */\n const handleRecordingUpload = async (file: File) => {\n setRecordingUploading(true);\n setRecordingError(null);\n try {\n const { data: folderData } = await apollo.mutate({\n mutation: ENSURE_FOLDER,\n variables: { path: \"/TimeCampus/Recordings\" },\n });\n const folderId = (folderData as { ensureFolderByPath?: { id: string } } | null)\n ?.ensureFolderByPath?.id;\n if (!folderId) throw new Error(\"Could not resolve upload folder\");\n const uploaded = await uploadBlobToFiles({\n client: apollo,\n blob: file,\n filename: file.name,\n mimeType: file.type || \"application/octet-stream\",\n folderId,\n });\n const { data } = await apollo.query({\n query: GET_FILE_DOWNLOAD_URL,\n variables: { id: uploaded.id, expiresIn: 604800 },\n fetchPolicy: \"no-cache\",\n });\n const url = (data as { getFileDownloadUrl?: string } | null | undefined)?.getFileDownloadUrl;\n if (!url) throw new Error(\"No download URL returned\");\n setRecordingUrl(url);\n } catch {\n setRecordingError(\"Failed to upload recording. You can paste a URL instead.\");\n } finally {\n setRecordingUploading(false);\n }\n };\n\n const handleSubmit = async () => {\n if (!canSubmit) return;\n const startIso = new Date(start).toISOString();\n const endIso =\n duration > 0\n ? new Date(new Date(start).getTime() + duration * 60_000).toISOString()\n : undefined;\n\n if (isEdit && onUpdate) {\n await onUpdate({\n title: title.trim(),\n meetingType,\n startTime: startIso,\n endTime: endIso,\n notes: notes.trim() || undefined,\n recordingUrl: recordingUrl.trim() || undefined,\n });\n } else if (onCreate) {\n await onCreate({\n workspaceId,\n title: title.trim(),\n meetingType,\n startTime: startIso,\n endTime: endIso,\n notes: notes.trim() || undefined,\n recordingUrl: recordingUrl.trim() || undefined,\n });\n }\n };\n\n return (\n <Modal\n open={open}\n onClose={onClose}\n title={isEdit ? \"Edit Meeting\" : \"Schedule Meeting\"}\n description={\n isEdit\n ? \"Update this meeting's details.\"\n : \"Add a meeting to track its time, cost and follow-ups.\"\n }\n size=\"xl\"\n footer={\n <>\n <button\n onClick={onClose}\n className=\"rounded-md border border-border-subtle px-4 py-2 text-sm font-medium text-text-primary hover:bg-bg-sunken/50\"\n >\n Cancel\n </button>\n <button\n onClick={handleSubmit}\n disabled={!canSubmit}\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 {submitting\n ? isEdit\n ? \"Saving...\"\n : \"Scheduling...\"\n : isEdit\n ? \"Save Changes\"\n : \"Schedule\"}\n </button>\n </>\n }\n >\n {error && (\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 {isEdit\n ? \"Failed to update meeting. Please try again.\"\n : \"Failed to schedule meeting. Please try again.\"}\n </div>\n )}\n <div className=\"space-y-4\">\n <div>\n <label className={labelClass}>Meeting Title</label>\n <input\n type=\"text\"\n placeholder=\"Enter meeting title\"\n value={title}\n onChange={(e) => setTitle(e.target.value)}\n className={inputClass}\n />\n </div>\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2\">\n <div>\n <label className={labelClass}>Type</label>\n <select\n value={meetingType}\n onChange={(e) => setMeetingType(e.target.value as MeetingType)}\n className={inputClass}\n >\n {MEETING_TYPES.map((t) => (\n <option key={t.value} value={t.value}>\n {t.label}\n </option>\n ))}\n </select>\n </div>\n <div>\n <label className={labelClass}>Duration (minutes)</label>\n <input\n type=\"number\"\n min={0}\n value={duration}\n onChange={(e) => setDuration(Number(e.target.value))}\n className={inputClass}\n />\n </div>\n </div>\n <div>\n <label className={labelClass}>Start</label>\n <input\n type=\"datetime-local\"\n value={start}\n onChange={(e) => setStart(e.target.value)}\n className={inputClass}\n />\n </div>\n <div>\n <label className={labelClass}>Recording</label>\n <input\n type=\"file\"\n accept=\"audio/*,video/*\"\n disabled={recordingUploading}\n onChange={(e) => {\n const file = e.target.files?.[0];\n if (file) void handleRecordingUpload(file);\n // Reset so re-selecting the same file re-triggers onChange.\n e.target.value = \"\";\n }}\n className=\"mt-1 block w-full text-sm text-text-secondary file:mr-3 file:rounded-md file:border-0 file:bg-action-primary-bg file:px-3 file:py-1.5 file:text-sm file:font-medium file:text-action-primary-text hover:file:bg-action-primary-bg/90\"\n />\n {recordingUploading && (\n <p className=\"mt-1 text-xs text-text-secondary\">Uploading…</p>\n )}\n {recordingError && (\n <p className=\"mt-1 text-xs text-status-error-text\">\n {recordingError}\n </p>\n )}\n <input\n type=\"url\"\n placeholder=\"…or paste a recording URL (https://...)\"\n value={recordingUrl}\n onChange={(e) => setRecordingUrl(e.target.value)}\n className={inputClass}\n />\n </div>\n <div>\n <label className={labelClass}>Notes / Agenda</label>\n <textarea\n placeholder=\"Meeting agenda...\"\n rows={3}\n value={notes}\n onChange={(e) => setNotes(e.target.value)}\n className={inputClass}\n />\n </div>\n </div>\n </Modal>\n );\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"MeetingFormModal.js","names":[],"sources":["../../../../src/timecampus/pages/meetings/MeetingFormModal.tsx"],"sourcesContent":["import { useState, type FC } from \"react\";\nimport { gql } from \"@apollo/client\";\nimport { useApolloClient } from \"@apollo/client/react\";\nimport { uploadBlobToFiles } from \"@burdenoff/fe-libs/storage\";\nimport { Modal } from \"../../components/Modal\";\nimport {\n MeetingType,\n type CreateMeetingInput,\n type UpdateMeetingInput,\n} from \"@/generated/wspace-types\";\nimport type { MeetingFieldsFragment } from \"@/generated/wspace-operations\";\n\n/**\n * Resolve a downloadable URL for a Files object. The URL is presigned and\n * expires after 7 days (604800s) — a known limitation: a stored recordingUrl\n * will eventually 403 and the recording must be re-uploaded to refresh it.\n */\nconst GET_FILE_DOWNLOAD_URL = gql`\n query TimeCampus_GetFileDownloadUrl($id: ID!, $expiresIn: Int) {\n getFileDownloadUrl(id: $id, expiresIn: $expiresIn)\n }\n`;\n\n// wspace-files requires a real folder id (File.folderId FK).\nconst ENSURE_FOLDER = gql`\n mutation TimeCampusMeeting_EnsureFolder($path: String!) {\n ensureFolderByPath(path: $path) {\n id\n }\n }\n`;\n\nconst MEETING_TYPES: { value: MeetingType; label: string }[] = [\n { value: MeetingType.Team, label: \"Team\" },\n { value: MeetingType.Standup, label: \"Standup\" },\n { value: MeetingType.OneOnOne, label: \"One-on-one\" },\n { value: MeetingType.Planning, label: \"Planning\" },\n { value: MeetingType.Review, label: \"Review\" },\n { value: MeetingType.Retrospective, label: \"Retrospective\" },\n { value: MeetingType.External, label: \"External\" },\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\";\nconst labelClass = \"block text-sm font-medium text-text-primary\";\n\n/** Convert an ISO string into a value usable by a datetime-local input. */\nfunction toLocalInput(iso?: string | null): string {\n if (!iso) return \"\";\n const d = new Date(iso);\n if (Number.isNaN(d.getTime())) return \"\";\n const pad = (n: number) => String(n).padStart(2, \"0\");\n return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(\n d.getHours(),\n )}:${pad(d.getMinutes())}`;\n}\n\nexport interface MeetingFormValues {\n title: string;\n meetingType: MeetingType;\n startTime: string;\n endTime?: string;\n notes?: string;\n recordingUrl?: string;\n cost?: number;\n}\n\ninterface MeetingFormModalProps {\n open: boolean;\n onClose: () => void;\n /** Existing meeting when editing; undefined when creating. */\n meeting?: MeetingFieldsFragment;\n workspaceId: string;\n submitting: boolean;\n error?: boolean;\n /** Receives a ready-to-send input; caller wires it to the right mutation. */\n onCreate?: (input: CreateMeetingInput) => Promise<void> | void;\n onUpdate?: (input: UpdateMeetingInput) => Promise<void> | void;\n}\n\nexport const MeetingFormModal: FC<MeetingFormModalProps> = ({\n open,\n onClose,\n meeting,\n workspaceId,\n submitting,\n error,\n onCreate,\n onUpdate,\n}) => {\n const isEdit = Boolean(meeting);\n const [title, setTitle] = useState(meeting?.title ?? \"\");\n const [meetingType, setMeetingType] = useState<MeetingType>(\n (meeting?.meetingType as MeetingType | undefined) ?? MeetingType.Team,\n );\n const [start, setStart] = useState(toLocalInput(meeting?.startTime));\n const [duration, setDuration] = useState<number>(meeting?.duration ?? 30);\n const [notes, setNotes] = useState(meeting?.notes ?? \"\");\n const [recordingUrl, setRecordingUrl] = useState(meeting?.recordingUrl ?? \"\");\n const apollo = useApolloClient();\n const [recordingUploading, setRecordingUploading] = useState(false);\n const [recordingError, setRecordingError] = useState<string | null>(null);\n\n const canSubmit = Boolean(title.trim() && start) && !submitting;\n\n /**\n * Upload a recording (audio/video) to platform Files and store its presigned\n * URL in the existing recordingUrl state. Failures surface an inline error\n * but never block submitting the meeting — the recording is optional and the\n * manual URL field remains available as a fallback.\n */\n const handleRecordingUpload = async (file: File) => {\n setRecordingUploading(true);\n setRecordingError(null);\n try {\n const { data: folderData } = await apollo.mutate({\n mutation: ENSURE_FOLDER,\n variables: { path: \"/TimeCampus/Recordings\" },\n });\n const folderId = (\n folderData as { ensureFolderByPath?: { id: string } } | null\n )?.ensureFolderByPath?.id;\n if (!folderId) throw new Error(\"Could not resolve upload folder\");\n const uploaded = await uploadBlobToFiles({\n client: apollo,\n blob: file,\n filename: file.name,\n mimeType: file.type || \"application/octet-stream\",\n folderId,\n });\n const { data } = await apollo.query({\n query: GET_FILE_DOWNLOAD_URL,\n variables: { id: uploaded.id, expiresIn: 604800 },\n fetchPolicy: \"no-cache\",\n });\n const url = (data as { getFileDownloadUrl?: string } | null | undefined)\n ?.getFileDownloadUrl;\n if (!url) throw new Error(\"No download URL returned\");\n setRecordingUrl(url);\n } catch {\n setRecordingError(\n \"Failed to upload recording. You can paste a URL instead.\",\n );\n } finally {\n setRecordingUploading(false);\n }\n };\n\n const handleSubmit = async () => {\n if (!canSubmit) return;\n const startIso = new Date(start).toISOString();\n const endIso =\n duration > 0\n ? new Date(new Date(start).getTime() + duration * 60_000).toISOString()\n : undefined;\n\n if (isEdit && onUpdate) {\n await onUpdate({\n title: title.trim(),\n meetingType,\n startTime: startIso,\n endTime: endIso,\n notes: notes.trim() || undefined,\n recordingUrl: recordingUrl.trim() || undefined,\n });\n } else if (onCreate) {\n await onCreate({\n workspaceId,\n title: title.trim(),\n meetingType,\n startTime: startIso,\n endTime: endIso,\n notes: notes.trim() || undefined,\n recordingUrl: recordingUrl.trim() || undefined,\n });\n }\n };\n\n return (\n <Modal\n open={open}\n onClose={onClose}\n title={isEdit ? \"Edit Meeting\" : \"Schedule Meeting\"}\n description={\n isEdit\n ? \"Update this meeting's details.\"\n : \"Add a meeting to track its time, cost and follow-ups.\"\n }\n size=\"xl\"\n footer={\n <>\n <button\n onClick={onClose}\n className=\"rounded-md border border-border-subtle px-4 py-2 text-sm font-medium text-text-primary hover:bg-bg-sunken/50\"\n >\n Cancel\n </button>\n <button\n onClick={handleSubmit}\n disabled={!canSubmit}\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 {submitting\n ? isEdit\n ? \"Saving...\"\n : \"Scheduling...\"\n : isEdit\n ? \"Save Changes\"\n : \"Schedule\"}\n </button>\n </>\n }\n >\n {error && (\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 {isEdit\n ? \"Failed to update meeting. Please try again.\"\n : \"Failed to schedule meeting. Please try again.\"}\n </div>\n )}\n <div className=\"space-y-4\">\n <div>\n <label className={labelClass}>Meeting Title</label>\n <input\n type=\"text\"\n placeholder=\"Enter meeting title\"\n value={title}\n onChange={(e) => setTitle(e.target.value)}\n className={inputClass}\n />\n </div>\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2\">\n <div>\n <label className={labelClass}>Type</label>\n <select\n value={meetingType}\n onChange={(e) => setMeetingType(e.target.value as MeetingType)}\n className={inputClass}\n >\n {MEETING_TYPES.map((t) => (\n <option key={t.value} value={t.value}>\n {t.label}\n </option>\n ))}\n </select>\n </div>\n <div>\n <label className={labelClass}>Duration (minutes)</label>\n <input\n type=\"number\"\n min={0}\n value={duration}\n onChange={(e) => setDuration(Number(e.target.value))}\n className={inputClass}\n />\n </div>\n </div>\n <div>\n <label className={labelClass}>Start</label>\n <input\n type=\"datetime-local\"\n value={start}\n onChange={(e) => setStart(e.target.value)}\n className={inputClass}\n />\n </div>\n <div>\n <label className={labelClass}>Recording</label>\n <input\n type=\"file\"\n accept=\"audio/*,video/*\"\n disabled={recordingUploading}\n onChange={(e) => {\n const file = e.target.files?.[0];\n if (file) void handleRecordingUpload(file);\n // Reset so re-selecting the same file re-triggers onChange.\n e.target.value = \"\";\n }}\n className=\"mt-1 block w-full text-sm text-text-secondary file:mr-3 file:rounded-md file:border-0 file:bg-action-primary-bg file:px-3 file:py-1.5 file:text-sm file:font-medium file:text-action-primary-text hover:file:bg-action-primary-bg/90\"\n />\n {recordingUploading && (\n <p className=\"mt-1 text-xs text-text-secondary\">Uploading…</p>\n )}\n {recordingError && (\n <p className=\"mt-1 text-xs text-status-error-text\">\n {recordingError}\n </p>\n )}\n <input\n type=\"url\"\n placeholder=\"…or paste a recording URL (https://...)\"\n value={recordingUrl}\n onChange={(e) => setRecordingUrl(e.target.value)}\n className={inputClass}\n />\n </div>\n <div>\n <label className={labelClass}>Notes / Agenda</label>\n <textarea\n placeholder=\"Meeting agenda...\"\n rows={3}\n value={notes}\n onChange={(e) => setNotes(e.target.value)}\n className={inputClass}\n />\n </div>\n </div>\n </Modal>\n );\n};\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"PomodoroAnalyticsPage.js","names":[],"sources":["../../../../src/timecampus/pages/pomodoro/PomodoroAnalyticsPage.tsx"],"sourcesContent":["import { BarChart3, Target, Flame, Timer, Coffee } from \"lucide-react\";\nimport { GlassCard } from \"@burdenoff/fe-libs/ui\";\nimport {\n PageLayout,\n StatCard,\n ChartCard,\n BarsChart,\n DonutChart,\n ProgressMeter,\n StatusBadge,\n LoadingSkeleton,\n formatNumber,\n type DonutSlice,\n} from \"../../components\";\nimport { useTimeCampus } from \"../../providers/TimeCampusProvider\";\nimport {\n useGetPomodoroStatsQuery,\n useListPomodoroSessionsQuery,\n type PomodoroSessionFieldsFragment,\n} from \"@/generated/wspace-operations\";\nimport { PomodoroSessionType } from \"@/generated/wspace-types\";\n\nfunction formatMinutes(min: number): string {\n const total = Math.max(0, Math.round(min));\n const h = Math.floor(total / 60);\n const m = total % 60;\n return h > 0 ? `${h}h ${m}m` : `${m}m`;\n}\n\nfunction shortDay(ts: number): string {\n return new Date(ts).toLocaleDateString(undefined, {\n month: \"short\",\n day: \"numeric\",\n });\n}\n\nfunction shortTime(iso: string): string {\n const d = new Date(iso);\n if (Number.isNaN(d.getTime())) return \"\";\n return d.toLocaleTimeString(undefined, {\n hour: \"2-digit\",\n minute: \"2-digit\",\n });\n}\n\nconst isBreak = (t: PomodoroSessionType): boolean =>\n t === PomodoroSessionType.ShortBreak || t === PomodoroSessionType.LongBreak;\n\nexport function PomodoroAnalyticsPage() {\n const { workspaceId = \"\" } = useTimeCampus();\n\n const {\n data: statsData,\n loading: statsLoading,\n error: statsError,\n } = useGetPomodoroStatsQuery({\n variables: { workspaceId },\n skip: !workspaceId,\n });\n\n const { data: sessionsData, loading: sessionsLoading } =\n useListPomodoroSessionsQuery({\n variables: { workspaceId, limit: 50 },\n skip: !workspaceId,\n });\n\n const stats = statsData?.pomodoroStats;\n const sessions = sessionsData?.pomodoroSessions ?? [];\n\n if (statsLoading || sessionsLoading) {\n return (\n <PageLayout\n title=\"Pomodoro Analytics\"\n description=\"Track your focus sessions and productivity\"\n >\n <LoadingSkeleton variant=\"card\" count={4} />\n </PageLayout>\n );\n }\n\n if (statsError) {\n return (\n <PageLayout\n title=\"Pomodoro Analytics\"\n description=\"Track your focus sessions and productivity\"\n >\n <div className=\"rounded-lg border border-status-error-bg/40 bg-status-error-bg p-4 text-sm text-status-error-text\">\n Failed to load pomodoro analytics. Please try again later.\n </div>\n </PageLayout>\n );\n }\n\n const totalFocusH = Math.floor((stats?.totalFocusMinutes ?? 0) / 60);\n const totalFocusM = (stats?.totalFocusMinutes ?? 0) % 60;\n const avgSessions = stats?.totalSessions\n ? (stats.totalSessions / 30).toFixed(1)\n : \"0\";\n\n // ---- Daily completed focus sessions (BarsChart) --------------------------\n const dailyMap = new Map<\n string,\n { ts: number; label: string; completed: number }\n >();\n for (const s of sessions) {\n if (s.type !== PomodoroSessionType.Focus) continue;\n const d = new Date(s.startTime);\n if (Number.isNaN(d.getTime())) continue;\n const day = new Date(d.getFullYear(), d.getMonth(), d.getDate());\n const key = String(day.getTime());\n const entry = dailyMap.get(key) ?? {\n ts: day.getTime(),\n label: shortDay(day.getTime()),\n completed: 0,\n };\n if (s.completed) entry.completed += 1;\n dailyMap.set(key, entry);\n }\n const dailyData = [...dailyMap.values()]\n .sort((a, b) => a.ts - b.ts)\n .slice(-14)\n .map((e) => ({ day: e.label, completed: e.completed }));\n\n // ---- Focus vs break split (DonutChart) -----------------------------------\n let focusSeconds = 0;\n let breakSeconds = 0;\n let focusCount = 0;\n let breakCount = 0;\n for (const s of sessions) {\n const dur =\n typeof s.duration === \"number\" && Number.isFinite(s.duration)\n ? s.duration\n : 0;\n if (isBreak(s.type)) {\n breakSeconds += dur;\n breakCount += 1;\n } else {\n focusSeconds += dur;\n focusCount += 1;\n }\n }\n const useDuration = focusSeconds + breakSeconds > 0;\n const focusValue = useDuration ? Math.round(focusSeconds / 60) : focusCount;\n const breakValue = useDuration ? Math.round(breakSeconds / 60) : breakCount;\n const donutSlices: DonutSlice[] = [\n { name: \"Focus\", value: focusValue },\n { name: \"Break\", value: breakValue },\n ];\n const donutFormat = useDuration ? formatMinutes : formatNumber;\n\n // ---- Recent sessions -----------------------------------------------------\n const recentSessions = [...sessions]\n .sort(\n (a, b) => new Date(b.startTime).getTime() - new Date(a.startTime).getTime(),\n )\n .slice(0, 6);\n\n const completionRate = stats?.completionRate ?? 0;\n const completedSessions = stats?.completedSessions ?? 0;\n const totalSessions = stats?.totalSessions ?? 0;\n\n return (\n <PageLayout\n title=\"Pomodoro Analytics\"\n description=\"Track your focus sessions and productivity\"\n purpose=\"See whether your focus habit is sticking — completion rate, streaks and focus minutes over time — so you can tell good-focus days from distracted ones and keep your momentum going.\"\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=\"Total Sessions\"\n value={String(stats?.totalSessions ?? 0)}\n trend={{\n value: stats?.completionRate ?? 0,\n label: \"completion rate\",\n }}\n />\n <StatCard\n title=\"Total Focus Time\"\n value={`${totalFocusH}h ${totalFocusM}m`}\n icon={<Timer className=\"h-5 w-5\" />}\n trend={{\n value: stats?.averageSessionMinutes ?? 0,\n label: \"avg min/session\",\n }}\n />\n <StatCard\n title=\"Avg. Sessions/Day\"\n value={avgSessions}\n icon={<BarChart3 className=\"h-5 w-5\" />}\n description={`Target: ${stats?.totalSessions ?? 0} total`}\n />\n <StatCard\n title=\"Completion Rate\"\n value={`${stats?.completionRate ?? 0}%`}\n icon={<Target className=\"h-5 w-5\" />}\n trend={{ value: stats?.streakDays ?? 0, label: \"day streak\" }}\n />\n </div>\n\n <div className=\"grid grid-cols-1 gap-6 lg:grid-cols-3\">\n <ChartCard\n className=\"lg:col-span-2\"\n title=\"Daily Completed Focus Sessions\"\n description=\"Pomodoros you finished, by day\"\n >\n <BarsChart\n data={dailyData}\n xKey=\"day\"\n series={[{ key: \"completed\", label: \"Completed\" }]}\n height={260}\n emptyLabel=\"No completed focus sessions yet.\"\n ariaLabel=\"Completed focus sessions by day\"\n />\n </ChartCard>\n\n <GlassCard treatment=\"glass\" glow className=\"p-5 sm:p-6\">\n <div className=\"flex items-center gap-2 text-sm font-semibold text-text-primary\">\n <Flame className=\"h-4 w-4 text-primary\" />\n Current Focus Streak\n </div>\n <p className=\"mt-2 text-4xl font-bold tabular-nums text-text-primary\">\n {stats?.streakDays ?? 0}\n <span className=\"ml-1 text-lg font-medium text-text-secondary\">\n days\n </span>\n </p>\n <div className=\"mt-6 space-y-5\">\n <ProgressMeter\n value={completionRate}\n max={100}\n higherIsWorse={false}\n label=\"Completion rate\"\n format={(v) => `${Math.round(v)}%`}\n />\n <ProgressMeter\n value={completedSessions}\n max={totalSessions > 0 ? totalSessions : 1}\n higherIsWorse={false}\n label=\"Sessions completed\"\n />\n </div>\n </GlassCard>\n </div>\n\n <div className=\"grid grid-cols-1 gap-6 lg:grid-cols-2\">\n <ChartCard\n title=\"Focus vs Break\"\n description={\n useDuration\n ? \"Share of tracked time spent focusing vs. on breaks\"\n : \"Share of sessions by type\"\n }\n >\n <DonutChart\n data={donutSlices}\n centerLabel={useDuration ? \"total tracked\" : \"sessions\"}\n valueFormat={donutFormat}\n height={260}\n emptyLabel=\"No sessions recorded yet.\"\n ariaLabel=\"Focus versus break split\"\n />\n </ChartCard>\n\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 <Timer className=\"h-4 w-4 text-primary\" />\n Recent Sessions\n </h3>\n {recentSessions.length === 0 ? (\n <p className=\"py-6 text-center text-sm text-text-secondary\">\n No focus sessions yet. Start a pomodoro to see it here.\n </p>\n ) : (\n <div className=\"space-y-2.5\">\n {recentSessions.map((s: PomodoroSessionFieldsFragment) => {\n const sessionIsBreak = isBreak(s.type);\n const durationLabel =\n typeof s.duration === \"number\" && Number.isFinite(s.duration)\n ? `${Math.round(s.duration / 60)}m`\n : \"--\";\n return (\n <div\n key={s.id}\n className=\"flex items-center gap-3 rounded-lg border border-border-subtle bg-bg-surface/40 p-3\"\n >\n <div\n className={`flex h-9 w-9 shrink-0 items-center justify-center rounded-lg ${\n sessionIsBreak\n ? \"bg-status-info-bg text-status-info-text\"\n : \"bg-action-primary-bg/10 text-primary\"\n }`}\n >\n {sessionIsBreak ? (\n <Coffee className=\"h-4 w-4\" />\n ) : (\n <Timer className=\"h-4 w-4\" />\n )}\n </div>\n <div className=\"min-w-0 flex-1\">\n <p className=\"truncate text-sm font-medium text-text-primary\">\n {s.notes?.trim()\n ? s.notes\n : sessionIsBreak\n ? \"Break\"\n : \"Focus session\"}\n </p>\n <p className=\"text-xs text-text-secondary\">\n {durationLabel} · {shortTime(s.startTime)}\n </p>\n </div>\n <StatusBadge\n variant={\n s.completed\n ? \"success\"\n : s.interrupted\n ? \"warning\"\n : \"muted\"\n }\n label={\n s.completed\n ? \"Completed\"\n : s.interrupted\n ? \"Interrupted\"\n : \"In progress\"\n }\n />\n </div>\n );\n })}\n </div>\n )}\n </GlassCard>\n </div>\n </div>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"PomodoroAnalyticsPage.js","names":[],"sources":["../../../../src/timecampus/pages/pomodoro/PomodoroAnalyticsPage.tsx"],"sourcesContent":["import { BarChart3, Target, Flame, Timer, Coffee } from \"lucide-react\";\nimport { GlassCard } from \"@burdenoff/fe-libs/ui\";\nimport {\n PageLayout,\n StatCard,\n ChartCard,\n BarsChart,\n DonutChart,\n ProgressMeter,\n StatusBadge,\n LoadingSkeleton,\n formatNumber,\n type DonutSlice,\n} from \"../../components\";\nimport { useTimeCampus } from \"../../providers/TimeCampusProvider\";\nimport {\n useGetPomodoroStatsQuery,\n useListPomodoroSessionsQuery,\n type PomodoroSessionFieldsFragment,\n} from \"@/generated/wspace-operations\";\nimport { PomodoroSessionType } from \"@/generated/wspace-types\";\n\nfunction formatMinutes(min: number): string {\n const total = Math.max(0, Math.round(min));\n const h = Math.floor(total / 60);\n const m = total % 60;\n return h > 0 ? `${h}h ${m}m` : `${m}m`;\n}\n\nfunction shortDay(ts: number): string {\n return new Date(ts).toLocaleDateString(undefined, {\n month: \"short\",\n day: \"numeric\",\n });\n}\n\nfunction shortTime(iso: string): string {\n const d = new Date(iso);\n if (Number.isNaN(d.getTime())) return \"\";\n return d.toLocaleTimeString(undefined, {\n hour: \"2-digit\",\n minute: \"2-digit\",\n });\n}\n\nconst isBreak = (t: PomodoroSessionType): boolean =>\n t === PomodoroSessionType.ShortBreak || t === PomodoroSessionType.LongBreak;\n\nexport function PomodoroAnalyticsPage() {\n const { workspaceId = \"\" } = useTimeCampus();\n\n const {\n data: statsData,\n loading: statsLoading,\n error: statsError,\n } = useGetPomodoroStatsQuery({\n variables: { workspaceId },\n skip: !workspaceId,\n });\n\n const { data: sessionsData, loading: sessionsLoading } =\n useListPomodoroSessionsQuery({\n variables: { workspaceId, limit: 50 },\n skip: !workspaceId,\n });\n\n const stats = statsData?.pomodoroStats;\n const sessions = sessionsData?.pomodoroSessions ?? [];\n\n if (statsLoading || sessionsLoading) {\n return (\n <PageLayout\n title=\"Pomodoro Analytics\"\n description=\"Track your focus sessions and productivity\"\n >\n <LoadingSkeleton variant=\"card\" count={4} />\n </PageLayout>\n );\n }\n\n if (statsError) {\n return (\n <PageLayout\n title=\"Pomodoro Analytics\"\n description=\"Track your focus sessions and productivity\"\n >\n <div className=\"rounded-lg border border-status-error-bg/40 bg-status-error-bg p-4 text-sm text-status-error-text\">\n Failed to load pomodoro analytics. Please try again later.\n </div>\n </PageLayout>\n );\n }\n\n const totalFocusH = Math.floor((stats?.totalFocusMinutes ?? 0) / 60);\n const totalFocusM = (stats?.totalFocusMinutes ?? 0) % 60;\n const avgSessions = stats?.totalSessions\n ? (stats.totalSessions / 30).toFixed(1)\n : \"0\";\n\n // ---- Daily completed focus sessions (BarsChart) --------------------------\n const dailyMap = new Map<\n string,\n { ts: number; label: string; completed: number }\n >();\n for (const s of sessions) {\n if (s.type !== PomodoroSessionType.Focus) continue;\n const d = new Date(s.startTime);\n if (Number.isNaN(d.getTime())) continue;\n const day = new Date(d.getFullYear(), d.getMonth(), d.getDate());\n const key = String(day.getTime());\n const entry = dailyMap.get(key) ?? {\n ts: day.getTime(),\n label: shortDay(day.getTime()),\n completed: 0,\n };\n if (s.completed) entry.completed += 1;\n dailyMap.set(key, entry);\n }\n const dailyData = [...dailyMap.values()]\n .sort((a, b) => a.ts - b.ts)\n .slice(-14)\n .map((e) => ({ day: e.label, completed: e.completed }));\n\n // ---- Focus vs break split (DonutChart) -----------------------------------\n let focusSeconds = 0;\n let breakSeconds = 0;\n let focusCount = 0;\n let breakCount = 0;\n for (const s of sessions) {\n const dur =\n typeof s.duration === \"number\" && Number.isFinite(s.duration)\n ? s.duration\n : 0;\n if (isBreak(s.type)) {\n breakSeconds += dur;\n breakCount += 1;\n } else {\n focusSeconds += dur;\n focusCount += 1;\n }\n }\n const useDuration = focusSeconds + breakSeconds > 0;\n const focusValue = useDuration ? Math.round(focusSeconds / 60) : focusCount;\n const breakValue = useDuration ? Math.round(breakSeconds / 60) : breakCount;\n const donutSlices: DonutSlice[] = [\n { name: \"Focus\", value: focusValue },\n { name: \"Break\", value: breakValue },\n ];\n const donutFormat = useDuration ? formatMinutes : formatNumber;\n\n // ---- Recent sessions -----------------------------------------------------\n const recentSessions = [...sessions]\n .sort(\n (a, b) =>\n new Date(b.startTime).getTime() - new Date(a.startTime).getTime(),\n )\n .slice(0, 6);\n\n const completionRate = stats?.completionRate ?? 0;\n const completedSessions = stats?.completedSessions ?? 0;\n const totalSessions = stats?.totalSessions ?? 0;\n\n return (\n <PageLayout\n title=\"Pomodoro Analytics\"\n description=\"Track your focus sessions and productivity\"\n purpose=\"See whether your focus habit is sticking — completion rate, streaks and focus minutes over time — so you can tell good-focus days from distracted ones and keep your momentum going.\"\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=\"Total Sessions\"\n value={String(stats?.totalSessions ?? 0)}\n trend={{\n value: stats?.completionRate ?? 0,\n label: \"completion rate\",\n }}\n />\n <StatCard\n title=\"Total Focus Time\"\n value={`${totalFocusH}h ${totalFocusM}m`}\n icon={<Timer className=\"h-5 w-5\" />}\n trend={{\n value: stats?.averageSessionMinutes ?? 0,\n label: \"avg min/session\",\n }}\n />\n <StatCard\n title=\"Avg. Sessions/Day\"\n value={avgSessions}\n icon={<BarChart3 className=\"h-5 w-5\" />}\n description={`Target: ${stats?.totalSessions ?? 0} total`}\n />\n <StatCard\n title=\"Completion Rate\"\n value={`${stats?.completionRate ?? 0}%`}\n icon={<Target className=\"h-5 w-5\" />}\n trend={{ value: stats?.streakDays ?? 0, label: \"day streak\" }}\n />\n </div>\n\n <div className=\"grid grid-cols-1 gap-6 lg:grid-cols-3\">\n <ChartCard\n className=\"lg:col-span-2\"\n title=\"Daily Completed Focus Sessions\"\n description=\"Pomodoros you finished, by day\"\n >\n <BarsChart\n data={dailyData}\n xKey=\"day\"\n series={[{ key: \"completed\", label: \"Completed\" }]}\n height={260}\n emptyLabel=\"No completed focus sessions yet.\"\n ariaLabel=\"Completed focus sessions by day\"\n />\n </ChartCard>\n\n <GlassCard treatment=\"glass\" glow className=\"p-5 sm:p-6\">\n <div className=\"flex items-center gap-2 text-sm font-semibold text-text-primary\">\n <Flame className=\"h-4 w-4 text-primary\" />\n Current Focus Streak\n </div>\n <p className=\"mt-2 text-4xl font-bold tabular-nums text-text-primary\">\n {stats?.streakDays ?? 0}\n <span className=\"ml-1 text-lg font-medium text-text-secondary\">\n days\n </span>\n </p>\n <div className=\"mt-6 space-y-5\">\n <ProgressMeter\n value={completionRate}\n max={100}\n higherIsWorse={false}\n label=\"Completion rate\"\n format={(v) => `${Math.round(v)}%`}\n />\n <ProgressMeter\n value={completedSessions}\n max={totalSessions > 0 ? totalSessions : 1}\n higherIsWorse={false}\n label=\"Sessions completed\"\n />\n </div>\n </GlassCard>\n </div>\n\n <div className=\"grid grid-cols-1 gap-6 lg:grid-cols-2\">\n <ChartCard\n title=\"Focus vs Break\"\n description={\n useDuration\n ? \"Share of tracked time spent focusing vs. on breaks\"\n : \"Share of sessions by type\"\n }\n >\n <DonutChart\n data={donutSlices}\n centerLabel={useDuration ? \"total tracked\" : \"sessions\"}\n valueFormat={donutFormat}\n height={260}\n emptyLabel=\"No sessions recorded yet.\"\n ariaLabel=\"Focus versus break split\"\n />\n </ChartCard>\n\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 <Timer className=\"h-4 w-4 text-primary\" />\n Recent Sessions\n </h3>\n {recentSessions.length === 0 ? (\n <p className=\"py-6 text-center text-sm text-text-secondary\">\n No focus sessions yet. Start a pomodoro to see it here.\n </p>\n ) : (\n <div className=\"space-y-2.5\">\n {recentSessions.map((s: PomodoroSessionFieldsFragment) => {\n const sessionIsBreak = isBreak(s.type);\n const durationLabel =\n typeof s.duration === \"number\" &&\n Number.isFinite(s.duration)\n ? `${Math.round(s.duration / 60)}m`\n : \"--\";\n return (\n <div\n key={s.id}\n className=\"flex items-center gap-3 rounded-lg border border-border-subtle bg-bg-surface/40 p-3\"\n >\n <div\n className={`flex h-9 w-9 shrink-0 items-center justify-center rounded-lg ${\n sessionIsBreak\n ? \"bg-status-info-bg text-status-info-text\"\n : \"bg-action-primary-bg/10 text-primary\"\n }`}\n >\n {sessionIsBreak ? (\n <Coffee className=\"h-4 w-4\" />\n ) : (\n <Timer className=\"h-4 w-4\" />\n )}\n </div>\n <div className=\"min-w-0 flex-1\">\n <p className=\"truncate text-sm font-medium text-text-primary\">\n {s.notes?.trim()\n ? s.notes\n : sessionIsBreak\n ? \"Break\"\n : \"Focus session\"}\n </p>\n <p className=\"text-xs text-text-secondary\">\n {durationLabel} · {shortTime(s.startTime)}\n </p>\n </div>\n <StatusBadge\n variant={\n s.completed\n ? \"success\"\n : s.interrupted\n ? \"warning\"\n : \"muted\"\n }\n label={\n s.completed\n ? \"Completed\"\n : s.interrupted\n ? \"Interrupted\"\n : \"In progress\"\n }\n />\n </div>\n );\n })}\n </div>\n )}\n </GlassCard>\n </div>\n </div>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"TeamAnalyticsPage.js","names":[],"sources":["../../../../src/timecampus/pages/team/TeamAnalyticsPage.tsx"],"sourcesContent":["import { BarChart3, Users, Clock, TrendingUp, Lightbulb } from \"lucide-react\";\nimport { GlassCard } from \"@burdenoff/fe-libs/ui\";\nimport {\n PageLayout,\n StatCard,\n ChartCard,\n StatusBadge,\n LoadingSkeleton,\n BarsChart,\n DonutChart,\n RadialGauge,\n} from \"../../components\";\nimport {\n useTimeCampus,\n useTimeCampusNavigate,\n} from \"../../providers/TimeCampusProvider\";\nimport {\n useGetTimeEntryStatsQuery,\n useGetWorkPatternSummaryQuery,\n} from \"@/generated/wspace-operations\";\n\nconst formatHrs = (v: number): string =>\n `${Number.isInteger(v) ? v : v.toFixed(1)}h`;\n\nconst impactVariant = (\n impact?: string | null,\n): \"success\" | \"warning\" | \"info\" | \"muted\" => {\n const value = (impact ?? \"\").toLowerCase();\n if (value.includes(\"high\") || value.includes(\"critical\")) return \"warning\";\n if (value.includes(\"low\")) return \"muted\";\n if (value.includes(\"positive\") || value.includes(\"good\")) return \"success\";\n return \"info\";\n};\n\nexport function TeamAnalyticsPage() {\n const { workspaceId = \"\" } = useTimeCampus();\n const navigate = useTimeCampusNavigate();\n\n const analyticsActions = (\n <div className=\"flex items-center gap-2\">\n <button\n onClick={() => navigate(\"/team\")}\n className=\"inline-flex items-center gap-1.5 rounded-md border border-border-subtle px-3 py-2 text-sm font-medium text-text-primary hover:bg-bg-sunken/50\"\n >\n Team Overview\n </button>\n <button\n onClick={() => navigate(\"/team/members\")}\n className=\"inline-flex items-center gap-1.5 rounded-md bg-action-primary-bg px-3 py-2 text-sm font-medium text-action-primary-text hover:bg-action-primary-bg/90\"\n >\n <Users className=\"h-4 w-4\" />\n View Members\n </button>\n </div>\n );\n\n const {\n data: statsData,\n loading: statsLoading,\n error: statsError,\n } = useGetTimeEntryStatsQuery({\n skip: !workspaceId,\n });\n\n const { data: patternData, loading: patternLoading } =\n useGetWorkPatternSummaryQuery({\n skip: !workspaceId,\n });\n\n const stats = statsData?.timeEntryStats;\n const pattern = patternData?.workPatternSummary;\n\n if (statsLoading || patternLoading) {\n return (\n <PageLayout\n title=\"Team Analytics\"\n description=\"Team performance insights and metrics\"\n >\n <LoadingSkeleton variant=\"card\" count={4} />\n </PageLayout>\n );\n }\n\n if (statsError) {\n return (\n <PageLayout\n title=\"Team Analytics\"\n description=\"Team performance insights and metrics\"\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 team analytics. Please try again later.\n </div>\n </PageLayout>\n );\n }\n\n const totalHours = stats?.totalHours ?? 0;\n const billableHours = stats?.billableHours ?? 0;\n const nonBillableHours = Math.max(0, totalHours - billableHours);\n const billablePercent =\n totalHours > 0 ? Math.round((billableHours / totalHours) * 100) : 0;\n\n const avgFocus = pattern?.averageFocusHours ?? 0;\n const avgMeeting = pattern?.averageMeetingHours ?? 0;\n const avgProductivity = pattern?.averageProductivity ?? 0;\n const insights = pattern?.insights ?? [];\n\n // Aggregate team composition — never individual — from the fetched stats.\n const distributionData = [\n {\n label: \"Logged\",\n billable: Math.round(billableHours * 10) / 10,\n nonBillable: Math.round(nonBillableHours * 10) / 10,\n },\n ];\n\n // Focus vs meeting balance from the work-pattern summary (avg per person).\n const balanceData = [\n { name: \"Deep Work / Focus\", value: Math.round(avgFocus * 10) / 10 },\n { name: \"Meetings\", value: Math.round(avgMeeting * 10) / 10 },\n ];\n\n return (\n <PageLayout\n title=\"Team Analytics\"\n description=\"Team performance insights and metrics\"\n purpose=\"Compare how your team works — hours by member, productive days, focus versus meeting time — so you can rebalance workload, recognise top contributors, and fix the patterns that slow everyone down.\"\n actions={analyticsActions}\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=\"Total Team Hours\"\n value={formatHrs(Math.round(totalHours))}\n icon={<Clock className=\"h-5 w-5\" />}\n />\n <StatCard\n title=\"Billable Hours\"\n value={formatHrs(Math.round(billableHours))}\n icon={<TrendingUp className=\"h-5 w-5\" />}\n trend={{ value: billablePercent, label: \"billable rate\" }}\n />\n <StatCard\n title=\"Avg Productivity\"\n value={`${Math.round(avgProductivity)}%`}\n icon={<BarChart3 className=\"h-5 w-5\" />}\n iconBgColor=\"bg-status-success-bg text-status-success-text\"\n />\n <StatCard\n title=\"Meeting Load\"\n value={formatHrs(avgMeeting)}\n icon={<Users className=\"h-5 w-5\" />}\n description=\"Avg / person — the true cost of meetings\"\n />\n </div>\n\n <div className=\"grid grid-cols-1 gap-6 lg:grid-cols-2\">\n <ChartCard\n title=\"Team Time Distribution\"\n description=\"All data aggregate, not individual.\"\n >\n <BarsChart\n data={distributionData}\n xKey=\"label\"\n orientation=\"horizontal\"\n stacked\n height={150}\n categoryWidth={72}\n valueFormat={formatHrs}\n series={[\n { key: \"billable\", label: \"Billable\" },\n { key: \"nonBillable\", label: \"Non-billable\" },\n ]}\n emptyLabel=\"No logged hours yet\"\n />\n <p className=\"mt-2 text-xs text-text-secondary\">\n {billablePercent}% of {formatHrs(Math.round(totalHours))} logged\n is billable.\n </p>\n </ChartCard>\n\n <ChartCard\n title=\"Focus vs Meeting Balance\"\n description=\"Average deep-work versus meeting time per person.\"\n >\n <DonutChart\n data={balanceData}\n centerValue={Math.round((avgFocus + avgMeeting) * 10) / 10}\n centerLabel=\"Avg / person\"\n valueFormat={formatHrs}\n height={220}\n emptyLabel=\"No work-pattern data yet\"\n />\n </ChartCard>\n </div>\n\n <div className=\"grid grid-cols-1 gap-6 lg:grid-cols-2\">\n <ChartCard\n title=\"Team Productivity\"\n description=\"Aggregate productivity score across the team.\"\n >\n <RadialGauge\n value={Math.round(avgProductivity)}\n min={0}\n max={100}\n label=\"Productivity\"\n sublabel={\n pattern?.mostProductiveDay\n ? `Peak day: ${pattern.mostProductiveDay}`\n : undefined\n }\n format={(v) => `${Math.round(v)}%`}\n height={220}\n />\n <div className=\"mt-4 flex flex-wrap items-center justify-center gap-2\">\n {pattern?.mostProductiveDay && (\n <StatusBadge\n variant=\"success\"\n label={`Most productive: ${pattern.mostProductiveDay}`}\n />\n )}\n {pattern?.leastProductiveDay && (\n <StatusBadge\n variant=\"muted\"\n label={`Quietest: ${pattern.leastProductiveDay}`}\n />\n )}\n </div>\n </ChartCard>\n\n <GlassCard treatment=\"glass\" glow className=\"p-4 sm:p-6\">\n <div className=\"mb-4 flex items-center gap-2\">\n <Lightbulb className=\"h-5 w-5 text-primary\" />\n <h3 className=\"text-sm font-semibold text-text-primary\">\n Team Insights\n </h3>\n </div>\n {insights.length === 0 ? (\n <div className=\"flex h-40 items-center justify-center rounded-lg border border-border-subtle bg-bg-sunken/30 text-center\">\n <p className=\"max-w-xs text-sm text-text-secondary\">\n Insights appear here as your team logs more work patterns.\n </p>\n </div>\n ) : (\n <ul className=\"space-y-3\">\n {insights.map((insight: (typeof insights)[number], index: number) => (\n <li\n key={`${insight.type}-${index}`}\n className=\"rounded-lg border border-border-subtle bg-bg-surface/40 p-3\"\n >\n <div className=\"flex items-start justify-between gap-3\">\n <p className=\"text-sm font-medium text-text-primary\">\n {insight.title}\n </p>\n {insight.impact && (\n <StatusBadge\n variant={impactVariant(insight.impact)}\n label={insight.impact}\n />\n )}\n </div>\n <p className=\"mt-1 text-xs text-text-secondary\">\n {insight.description}\n </p>\n {insight.recommendation && (\n <p className=\"mt-2 text-xs text-primary\">\n {insight.recommendation}\n </p>\n )}\n </li>\n ))}\n </ul>\n )}\n </GlassCard>\n </div>\n </div>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"TeamAnalyticsPage.js","names":[],"sources":["../../../../src/timecampus/pages/team/TeamAnalyticsPage.tsx"],"sourcesContent":["import { BarChart3, Users, Clock, TrendingUp, Lightbulb } from \"lucide-react\";\nimport { GlassCard } from \"@burdenoff/fe-libs/ui\";\nimport {\n PageLayout,\n StatCard,\n ChartCard,\n StatusBadge,\n LoadingSkeleton,\n BarsChart,\n DonutChart,\n RadialGauge,\n} from \"../../components\";\nimport {\n useTimeCampus,\n useTimeCampusNavigate,\n} from \"../../providers/TimeCampusProvider\";\nimport {\n useGetTimeEntryStatsQuery,\n useGetWorkPatternSummaryQuery,\n} from \"@/generated/wspace-operations\";\n\nconst formatHrs = (v: number): string =>\n `${Number.isInteger(v) ? v : v.toFixed(1)}h`;\n\nconst impactVariant = (\n impact?: string | null,\n): \"success\" | \"warning\" | \"info\" | \"muted\" => {\n const value = (impact ?? \"\").toLowerCase();\n if (value.includes(\"high\") || value.includes(\"critical\")) return \"warning\";\n if (value.includes(\"low\")) return \"muted\";\n if (value.includes(\"positive\") || value.includes(\"good\")) return \"success\";\n return \"info\";\n};\n\nexport function TeamAnalyticsPage() {\n const { workspaceId = \"\" } = useTimeCampus();\n const navigate = useTimeCampusNavigate();\n\n const analyticsActions = (\n <div className=\"flex items-center gap-2\">\n <button\n onClick={() => navigate(\"/team\")}\n className=\"inline-flex items-center gap-1.5 rounded-md border border-border-subtle px-3 py-2 text-sm font-medium text-text-primary hover:bg-bg-sunken/50\"\n >\n Team Overview\n </button>\n <button\n onClick={() => navigate(\"/team/members\")}\n className=\"inline-flex items-center gap-1.5 rounded-md bg-action-primary-bg px-3 py-2 text-sm font-medium text-action-primary-text hover:bg-action-primary-bg/90\"\n >\n <Users className=\"h-4 w-4\" />\n View Members\n </button>\n </div>\n );\n\n const {\n data: statsData,\n loading: statsLoading,\n error: statsError,\n } = useGetTimeEntryStatsQuery({\n skip: !workspaceId,\n });\n\n const { data: patternData, loading: patternLoading } =\n useGetWorkPatternSummaryQuery({\n skip: !workspaceId,\n });\n\n const stats = statsData?.timeEntryStats;\n const pattern = patternData?.workPatternSummary;\n\n if (statsLoading || patternLoading) {\n return (\n <PageLayout\n title=\"Team Analytics\"\n description=\"Team performance insights and metrics\"\n >\n <LoadingSkeleton variant=\"card\" count={4} />\n </PageLayout>\n );\n }\n\n if (statsError) {\n return (\n <PageLayout\n title=\"Team Analytics\"\n description=\"Team performance insights and metrics\"\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 team analytics. Please try again later.\n </div>\n </PageLayout>\n );\n }\n\n const totalHours = stats?.totalHours ?? 0;\n const billableHours = stats?.billableHours ?? 0;\n const nonBillableHours = Math.max(0, totalHours - billableHours);\n const billablePercent =\n totalHours > 0 ? Math.round((billableHours / totalHours) * 100) : 0;\n\n const avgFocus = pattern?.averageFocusHours ?? 0;\n const avgMeeting = pattern?.averageMeetingHours ?? 0;\n const avgProductivity = pattern?.averageProductivity ?? 0;\n const insights = pattern?.insights ?? [];\n\n // Aggregate team composition — never individual — from the fetched stats.\n const distributionData = [\n {\n label: \"Logged\",\n billable: Math.round(billableHours * 10) / 10,\n nonBillable: Math.round(nonBillableHours * 10) / 10,\n },\n ];\n\n // Focus vs meeting balance from the work-pattern summary (avg per person).\n const balanceData = [\n { name: \"Deep Work / Focus\", value: Math.round(avgFocus * 10) / 10 },\n { name: \"Meetings\", value: Math.round(avgMeeting * 10) / 10 },\n ];\n\n return (\n <PageLayout\n title=\"Team Analytics\"\n description=\"Team performance insights and metrics\"\n purpose=\"Compare how your team works — hours by member, productive days, focus versus meeting time — so you can rebalance workload, recognise top contributors, and fix the patterns that slow everyone down.\"\n actions={analyticsActions}\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=\"Total Team Hours\"\n value={formatHrs(Math.round(totalHours))}\n icon={<Clock className=\"h-5 w-5\" />}\n />\n <StatCard\n title=\"Billable Hours\"\n value={formatHrs(Math.round(billableHours))}\n icon={<TrendingUp className=\"h-5 w-5\" />}\n trend={{ value: billablePercent, label: \"billable rate\" }}\n />\n <StatCard\n title=\"Avg Productivity\"\n value={`${Math.round(avgProductivity)}%`}\n icon={<BarChart3 className=\"h-5 w-5\" />}\n iconBgColor=\"bg-status-success-bg text-status-success-text\"\n />\n <StatCard\n title=\"Meeting Load\"\n value={formatHrs(avgMeeting)}\n icon={<Users className=\"h-5 w-5\" />}\n description=\"Avg / person — the true cost of meetings\"\n />\n </div>\n\n <div className=\"grid grid-cols-1 gap-6 lg:grid-cols-2\">\n <ChartCard\n title=\"Team Time Distribution\"\n description=\"All data aggregate, not individual.\"\n >\n <BarsChart\n data={distributionData}\n xKey=\"label\"\n orientation=\"horizontal\"\n stacked\n height={150}\n categoryWidth={72}\n valueFormat={formatHrs}\n series={[\n { key: \"billable\", label: \"Billable\" },\n { key: \"nonBillable\", label: \"Non-billable\" },\n ]}\n emptyLabel=\"No logged hours yet\"\n />\n <p className=\"mt-2 text-xs text-text-secondary\">\n {billablePercent}% of {formatHrs(Math.round(totalHours))} logged\n is billable.\n </p>\n </ChartCard>\n\n <ChartCard\n title=\"Focus vs Meeting Balance\"\n description=\"Average deep-work versus meeting time per person.\"\n >\n <DonutChart\n data={balanceData}\n centerValue={Math.round((avgFocus + avgMeeting) * 10) / 10}\n centerLabel=\"Avg / person\"\n valueFormat={formatHrs}\n height={220}\n emptyLabel=\"No work-pattern data yet\"\n />\n </ChartCard>\n </div>\n\n <div className=\"grid grid-cols-1 gap-6 lg:grid-cols-2\">\n <ChartCard\n title=\"Team Productivity\"\n description=\"Aggregate productivity score across the team.\"\n >\n <RadialGauge\n value={Math.round(avgProductivity)}\n min={0}\n max={100}\n label=\"Productivity\"\n sublabel={\n pattern?.mostProductiveDay\n ? `Peak day: ${pattern.mostProductiveDay}`\n : undefined\n }\n format={(v) => `${Math.round(v)}%`}\n height={220}\n />\n <div className=\"mt-4 flex flex-wrap items-center justify-center gap-2\">\n {pattern?.mostProductiveDay && (\n <StatusBadge\n variant=\"success\"\n label={`Most productive: ${pattern.mostProductiveDay}`}\n />\n )}\n {pattern?.leastProductiveDay && (\n <StatusBadge\n variant=\"muted\"\n label={`Quietest: ${pattern.leastProductiveDay}`}\n />\n )}\n </div>\n </ChartCard>\n\n <GlassCard treatment=\"glass\" glow className=\"p-4 sm:p-6\">\n <div className=\"mb-4 flex items-center gap-2\">\n <Lightbulb className=\"h-5 w-5 text-primary\" />\n <h3 className=\"text-sm font-semibold text-text-primary\">\n Team Insights\n </h3>\n </div>\n {insights.length === 0 ? (\n <div className=\"flex h-40 items-center justify-center rounded-lg border border-border-subtle bg-bg-sunken/30 text-center\">\n <p className=\"max-w-xs text-sm text-text-secondary\">\n Insights appear here as your team logs more work patterns.\n </p>\n </div>\n ) : (\n <ul className=\"space-y-3\">\n {insights.map(\n (insight: (typeof insights)[number], index: number) => (\n <li\n key={`${insight.type}-${index}`}\n className=\"rounded-lg border border-border-subtle bg-bg-surface/40 p-3\"\n >\n <div className=\"flex items-start justify-between gap-3\">\n <p className=\"text-sm font-medium text-text-primary\">\n {insight.title}\n </p>\n {insight.impact && (\n <StatusBadge\n variant={impactVariant(insight.impact)}\n label={insight.impact}\n />\n )}\n </div>\n <p className=\"mt-1 text-xs text-text-secondary\">\n {insight.description}\n </p>\n {insight.recommendation && (\n <p className=\"mt-2 text-xs text-primary\">\n {insight.recommendation}\n </p>\n )}\n </li>\n ),\n )}\n </ul>\n )}\n </GlassCard>\n </div>\n </div>\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 Activity,\n Brain,\n Calendar,\n Coffee,\n Github,\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\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\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 timeline = data?.timeCampusTimeline;\n const segments = timeline?.segments ?? [];\n const summary = timeline?.summary;\n\n const totalsByCategory = useMemo(() => {\n const map = new Map<string, { totalSeconds: number; segmentCount: number }>();\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 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 <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 */}\n {segments.length > 0 && (\n <GlassCard treatment=\"glass\" className=\"p-4 sm:p-6\">\n <h2 className=\"mb-4 text-sm font-semibold text-text-primary\">\n Where the time went\n </h2>\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 <div\n key={category}\n className=\"flex items-center gap-2 rounded-lg border border-border-seam bg-bg-surface px-3 py-2\"\n >\n <span\n className={`inline-block h-2.5 w-2.5 rounded-full ${meta.dot}`}\n aria-hidden\n />\n <span className=\"text-sm font-medium text-text-primary\">\n {meta.label}\n </span>\n <span className=\"text-sm tabular-nums text-text-secondary\">\n {formatDuration(total.totalSeconds)}\n </span>\n </div>\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 capture channels, 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 const category = (segment.category as Category) ?? \"OTHER\";\n const meta = CATEGORY_META[category] ?? CATEGORY_META.OTHER;\n const SourceIcon = SOURCE_ICON[segment.source] ?? Activity;\n return (\n <li\n key={segment.id}\n className=\"flex items-start gap-3 p-4 transition-colors hover:bg-bg-sunken/50\"\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 <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\">\n {segment.app}\n </span>\n )}\n {segment.isRecategorizable && (\n <span className=\"text-xs text-text-tertiary\">\n via capture\n </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) =>\n handleRecategorize(\n segment,\n e.target.value as Category,\n )\n }\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 </ol>\n </GlassCard>\n )}\n </div>\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 Activity,\n Brain,\n Calendar,\n Coffee,\n Github,\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\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\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 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 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 <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 */}\n {segments.length > 0 && (\n <GlassCard treatment=\"glass\" className=\"p-4 sm:p-6\">\n <h2 className=\"mb-4 text-sm font-semibold text-text-primary\">\n Where the time went\n </h2>\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 <div\n key={category}\n className=\"flex items-center gap-2 rounded-lg border border-border-seam bg-bg-surface px-3 py-2\"\n >\n <span\n className={`inline-block h-2.5 w-2.5 rounded-full ${meta.dot}`}\n aria-hidden\n />\n <span className=\"text-sm font-medium text-text-primary\">\n {meta.label}\n </span>\n <span className=\"text-sm tabular-nums text-text-secondary\">\n {formatDuration(total.totalSeconds)}\n </span>\n </div>\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 capture channels, 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 const category = (segment.category as Category) ?? \"OTHER\";\n const meta = CATEGORY_META[category] ?? CATEGORY_META.OTHER;\n const SourceIcon = SOURCE_ICON[segment.source] ?? Activity;\n return (\n <li\n key={segment.id}\n className=\"flex items-start gap-3 p-4 transition-colors hover:bg-bg-sunken/50\"\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 <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\">\n {segment.app}\n </span>\n )}\n {segment.isRecategorizable && (\n <span className=\"text-xs text-text-tertiary\">\n via capture\n </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) =>\n handleRecategorize(\n segment,\n e.target.value as Category,\n )\n }\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 </ol>\n </GlassCard>\n )}\n </div>\n )}\n </PageLayout>\n );\n}\n"],"mappings":"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"}
@@ -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 {\n useTimeCampus,\n useTimeCampusNavigate,\n} 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 navigate = useTimeCampusNavigate();\n\n const {\n data: patternsData,\n loading: patternsLoading,\n error: patternsError,\n } = useListWorkPatternsQuery({\n variables: { 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: 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 <button\n type=\"button\"\n onClick={() => navigate(\"/work-patterns/settings\")}\n className=\"mt-4 inline-flex items-center gap-1.5 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 Adjust focus schedule\n <ArrowRight className=\"h-4 w-4\" />\n </button>\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 {\n useTimeCampus,\n useTimeCampusNavigate,\n} 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 navigate = useTimeCampusNavigate();\n\n const {\n data: patternsData,\n loading: patternsLoading,\n error: patternsError,\n } = useListWorkPatternsQuery({\n variables: { 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 <button\n type=\"button\"\n onClick={() => navigate(\"/work-patterns/settings\")}\n className=\"mt-4 inline-flex items-center gap-1.5 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 Adjust focus schedule\n <ArrowRight className=\"h-4 w-4\" />\n </button>\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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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@burdenoff/microfe-timecampus",
3
- "version": "2026.703.5",
3
+ "version": "2026.703.7",
4
4
  "description": "Burdenoff TimeCampus Microfrontend - AI-powered time management and productivity platform",
5
5
  "type": "module",
6
6
  "files": [