@burdenoff/microfe-timecampus 2026.827.1 → 2026.830.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,4 +4,5 @@
4
4
  * @module timecampus/hooks
5
5
  */
6
6
  export { useToast } from './useToast';
7
+ export { useTr } from '../shared/hooks/useTr';
7
8
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/timecampus/hooks/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/timecampus/hooks/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAGtC,OAAO,EAAE,KAAK,EAAE,MAAM,uBAAuB,CAAC"}
@@ -0,0 +1,3 @@
1
+ import "./useToast.js";
2
+ import { useTr as e } from "../shared/hooks/useTr.js";
3
+ export { e as useTr };
@@ -0,0 +1 @@
1
+ import "react";
@@ -1 +1 @@
1
- {"version":3,"file":"HomePage.d.ts","sourceRoot":"","sources":["../../../src/timecampus/pages/HomePage.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC;AA6BhC,eAAO,MAAM,QAAQ,EAAE,EA4WtB,CAAC"}
1
+ {"version":3,"file":"HomePage.d.ts","sourceRoot":"","sources":["../../../src/timecampus/pages/HomePage.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC;AA6BhC,eAAO,MAAM,QAAQ,EAAE,EAwWtB,CAAC"}
@@ -3,254 +3,252 @@ import { cn as t, formatHours as n } from "../utils/helpers.js";
3
3
  import { StatCard as r } from "../components/StatCard.js";
4
4
  import { LoadingSkeleton as i } from "../components/LoadingSkeleton.js";
5
5
  import { useGetTimeEntryStatsQuery as a, useListTimeEntriesQuery as o } from "../../generated/wspace-operations.js";
6
- import { ArrowRight as s, BarChart3 as c, CheckCircle as l, Clock as u, FolderOpen as d, Play as f, TrendingUp as p, Users as m } from "lucide-react";
7
- import { jsx as h, jsxs as g } from "react/jsx-runtime";
8
- import { AuroraBackground as _, CTAOverflowMenu as v, EmphasisPanel as y, GlassCard as b, GradientText as x } from "@burdenoff/fe-libs/ui";
9
- import { useI18n as S } from "@burdenoff/fe-libs/shared/providers/shell/I18nProvider";
6
+ import { useTr as s } from "../shared/hooks/useTr.js";
7
+ import "../hooks/index.js";
8
+ import { ArrowRight as c, BarChart3 as l, CheckCircle as u, Clock as d, FolderOpen as f, Play as p, TrendingUp as m, Users as h } from "lucide-react";
9
+ import { jsx as g, jsxs as _ } from "react/jsx-runtime";
10
+ import { AuroraBackground as v, CTAOverflowMenu as y, EmphasisPanel as b, GlassCard as x, GradientText as S } from "@burdenoff/fe-libs/ui";
10
11
  //#region src/timecampus/pages/HomePage.tsx
11
12
  var C = () => {
12
- let { t: C } = S(), w = (e, t) => {
13
- let n = C(e);
14
- return n === e ? t : n;
15
- }, T = e(), { data: E, loading: D } = a(), { data: O, loading: k } = o({ variables: { pagination: { limit: 4 } } }), A = E?.timeEntryStats, j = O?.timeEntries?.items ?? [], M = /* @__PURE__ */ h(v, {
13
+ let C = s(), w = e(), { data: T, loading: E } = a(), { data: D, loading: O } = o({ variables: { pagination: { limit: 4 } } }), k = T?.timeEntryStats, A = D?.timeEntries?.items ?? [], j = /* @__PURE__ */ g(y, {
16
14
  primary: {
17
- label: w("timecampus.home.actions.startTimer", "Start Timer"),
18
- onSelect: () => T("/time/timer"),
19
- icon: /* @__PURE__ */ h(f, { className: "h-4 w-4" })
15
+ label: C("timecampus.home.actions.startTimer", "Start Timer"),
16
+ onSelect: () => w("/time/timer"),
17
+ icon: /* @__PURE__ */ g(p, { className: "h-4 w-4" })
20
18
  },
21
19
  actions: [{
22
- label: w("timecampus.home.quickActions.viewProjects.label", "View Projects"),
23
- onSelect: () => T("/projects"),
24
- icon: /* @__PURE__ */ h(d, { className: "h-4 w-4" })
20
+ label: C("timecampus.home.quickActions.viewProjects.label", "View Projects"),
21
+ onSelect: () => w("/projects"),
22
+ icon: /* @__PURE__ */ g(f, { className: "h-4 w-4" })
25
23
  }, {
26
- label: w("timecampus.home.quickActions.reports.label", "Reports"),
27
- onSelect: () => T("/reports"),
28
- icon: /* @__PURE__ */ h(c, { className: "h-4 w-4" })
24
+ label: C("timecampus.home.quickActions.reports.label", "Reports"),
25
+ onSelect: () => w("/reports"),
26
+ icon: /* @__PURE__ */ g(l, { className: "h-4 w-4" })
29
27
  }]
30
- }), N = () => /* @__PURE__ */ g("div", {
28
+ }), M = () => /* @__PURE__ */ _("div", {
31
29
  className: "relative overflow-hidden rounded-2xl border border-border-seam bg-gloss-card p-5 sm:p-6",
32
- children: [/* @__PURE__ */ h(_, { intensity: "subtle" }), /* @__PURE__ */ g("div", {
30
+ children: [/* @__PURE__ */ g(v, { intensity: "subtle" }), /* @__PURE__ */ _("div", {
33
31
  className: "relative flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between",
34
- children: [/* @__PURE__ */ g("div", {
32
+ children: [/* @__PURE__ */ _("div", {
35
33
  className: "min-w-0",
36
- children: [/* @__PURE__ */ h("h1", {
34
+ children: [/* @__PURE__ */ g("h1", {
37
35
  className: "text-2xl font-bold text-text-primary",
38
- children: /* @__PURE__ */ h(x, { children: w("timecampus.home.title", "Dashboard") })
39
- }), /* @__PURE__ */ h("p", {
36
+ children: /* @__PURE__ */ g(S, { children: C("timecampus.home.title", "Dashboard") })
37
+ }), /* @__PURE__ */ g("p", {
40
38
  className: "mt-1 text-sm text-text-secondary",
41
- children: w("timecampus.home.description", "Overview of your time tracking and productivity")
39
+ children: C("timecampus.home.description", "Overview of your time tracking and productivity")
42
40
  })]
43
- }), M]
41
+ }), j]
44
42
  })]
45
43
  });
46
- return D && k ? /* @__PURE__ */ g("div", {
44
+ return E && O ? /* @__PURE__ */ _("div", {
47
45
  className: "min-h-full space-y-6 p-4 sm:p-6",
48
- children: [N(), /* @__PURE__ */ h(i, {
46
+ children: [M(), /* @__PURE__ */ g(i, {
49
47
  variant: "card",
50
48
  count: 4
51
49
  })]
52
- }) : /* @__PURE__ */ g("div", {
50
+ }) : /* @__PURE__ */ _("div", {
53
51
  className: "min-h-full space-y-6 p-4 sm:p-6",
54
52
  children: [
55
- N(),
56
- /* @__PURE__ */ g("div", {
53
+ M(),
54
+ /* @__PURE__ */ _("div", {
57
55
  className: "grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4",
58
56
  children: [
59
- /* @__PURE__ */ h(r, {
57
+ /* @__PURE__ */ g(r, {
60
58
  featured: !0,
61
- title: w("timecampus.home.stats.today", "Today"),
62
- value: n(A?.totalHours ?? 0),
63
- icon: /* @__PURE__ */ h(u, { className: "h-5 w-5" }),
59
+ title: C("timecampus.home.stats.today", "Today"),
60
+ value: n(k?.totalHours ?? 0),
61
+ icon: /* @__PURE__ */ g(d, { className: "h-5 w-5" }),
64
62
  trend: {
65
63
  value: 12.5,
66
- label: w("timecampus.home.stats.vsYesterday", "vs yesterday")
64
+ label: C("timecampus.home.stats.vsYesterday", "vs yesterday")
67
65
  }
68
66
  }),
69
- /* @__PURE__ */ h(r, {
70
- title: w("timecampus.home.stats.billable", "Billable"),
71
- value: n(A?.billableHours ?? 0),
72
- icon: /* @__PURE__ */ h(p, { className: "h-5 w-5" }),
73
- description: `${A?.totalEntries ?? 0} ${w("timecampus.home.stats.entries", "entries")}`
67
+ /* @__PURE__ */ g(r, {
68
+ title: C("timecampus.home.stats.billable", "Billable"),
69
+ value: n(k?.billableHours ?? 0),
70
+ icon: /* @__PURE__ */ g(m, { className: "h-5 w-5" }),
71
+ description: `${k?.totalEntries ?? 0} ${C("timecampus.home.stats.entries", "entries")}`
74
72
  }),
75
- /* @__PURE__ */ h(r, {
76
- title: w("timecampus.home.stats.activeTimers", "Active Timers"),
77
- value: A?.activeTimers ?? 0,
78
- icon: /* @__PURE__ */ h(d, { className: "h-5 w-5" }),
79
- description: w("timecampus.home.stats.currentlyRunning", "Currently running")
73
+ /* @__PURE__ */ g(r, {
74
+ title: C("timecampus.home.stats.activeTimers", "Active Timers"),
75
+ value: k?.activeTimers ?? 0,
76
+ icon: /* @__PURE__ */ g(f, { className: "h-5 w-5" }),
77
+ description: C("timecampus.home.stats.currentlyRunning", "Currently running")
80
78
  }),
81
- /* @__PURE__ */ h(r, {
82
- title: w("timecampus.home.stats.totalEntries", "Entries"),
83
- value: A?.totalEntries ?? 0,
84
- icon: /* @__PURE__ */ h(c, { className: "h-5 w-5" }),
85
- description: w("timecampus.home.stats.totalEntriesDescription", "Total time entries")
79
+ /* @__PURE__ */ g(r, {
80
+ title: C("timecampus.home.stats.totalEntries", "Entries"),
81
+ value: k?.totalEntries ?? 0,
82
+ icon: /* @__PURE__ */ g(l, { className: "h-5 w-5" }),
83
+ description: C("timecampus.home.stats.totalEntriesDescription", "Total time entries")
86
84
  })
87
85
  ]
88
86
  }),
89
- /* @__PURE__ */ g("div", {
87
+ /* @__PURE__ */ _("div", {
90
88
  className: "grid grid-cols-1 gap-6 lg:grid-cols-3",
91
- children: [/* @__PURE__ */ g(b, {
89
+ children: [/* @__PURE__ */ _(x, {
92
90
  treatment: "plain",
93
91
  className: "border-border-seam p-6 lg:col-span-2",
94
- children: [/* @__PURE__ */ g("div", {
92
+ children: [/* @__PURE__ */ _("div", {
95
93
  className: "mb-4 flex items-center justify-between",
96
- children: [/* @__PURE__ */ g("div", { children: [/* @__PURE__ */ h("h3", {
94
+ children: [/* @__PURE__ */ _("div", { children: [/* @__PURE__ */ g("h3", {
97
95
  className: "text-sm font-semibold text-text-primary",
98
- children: w("timecampus.home.recentEntries.title", "Recent Time Entries")
99
- }), /* @__PURE__ */ h("p", {
96
+ children: C("timecampus.home.recentEntries.title", "Recent Time Entries")
97
+ }), /* @__PURE__ */ g("p", {
100
98
  className: "text-xs text-text-secondary",
101
- children: w("timecampus.home.recentEntries.description", "Your latest tracked activities")
102
- })] }), /* @__PURE__ */ g("button", {
103
- onClick: () => T("/time/entries"),
99
+ children: C("timecampus.home.recentEntries.description", "Your latest tracked activities")
100
+ })] }), /* @__PURE__ */ _("button", {
101
+ onClick: () => w("/time/entries"),
104
102
  className: "inline-flex min-h-[2.75rem] items-center gap-1 rounded-md px-2 text-xs font-medium text-primary transition-colors hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-action-primary-border",
105
103
  children: [
106
- w("timecampus.home.recentEntries.viewAll", "View all"),
104
+ C("timecampus.home.recentEntries.viewAll", "View all"),
107
105
  " ",
108
- /* @__PURE__ */ h(s, { className: "h-3 w-3" })
106
+ /* @__PURE__ */ g(c, { className: "h-3 w-3" })
109
107
  ]
110
108
  })]
111
- }), k ? /* @__PURE__ */ h(i, {
109
+ }), O ? /* @__PURE__ */ g(i, {
112
110
  variant: "list",
113
111
  count: 4
114
- }) : /* @__PURE__ */ g("div", {
112
+ }) : /* @__PURE__ */ _("div", {
115
113
  className: "flex flex-col gap-3",
116
- children: [j.map((e) => /* @__PURE__ */ g("div", {
114
+ children: [A.map((e) => /* @__PURE__ */ _("div", {
117
115
  className: "flex items-center justify-between rounded-lg border border-border-seam p-3 transition-colors hover:bg-bg-sunken/50",
118
- children: [/* @__PURE__ */ g("div", {
116
+ children: [/* @__PURE__ */ _("div", {
119
117
  className: "min-w-0 flex-1",
120
- children: [/* @__PURE__ */ h("p", {
118
+ children: [/* @__PURE__ */ g("p", {
121
119
  className: "text-sm font-medium text-text-primary",
122
- children: e.project?.name ?? w("timecampus.home.recentEntries.noProject", "No Project")
123
- }), /* @__PURE__ */ h("p", {
120
+ children: e.project?.name ?? C("timecampus.home.recentEntries.noProject", "No Project")
121
+ }), /* @__PURE__ */ g("p", {
124
122
  className: "text-xs text-text-secondary",
125
- children: e.description ?? w("timecampus.home.recentEntries.noDescription", "No description")
123
+ children: e.description ?? C("timecampus.home.recentEntries.noDescription", "No description")
126
124
  })]
127
- }), /* @__PURE__ */ g("div", {
125
+ }), /* @__PURE__ */ _("div", {
128
126
  className: "ml-4 text-right",
129
- children: [/* @__PURE__ */ h("p", {
127
+ children: [/* @__PURE__ */ g("p", {
130
128
  className: "font-mono text-sm font-medium tabular-nums text-text-primary",
131
129
  children: n((e.duration ?? 0) / 3600)
132
- }), /* @__PURE__ */ g("span", {
130
+ }), /* @__PURE__ */ _("span", {
133
131
  className: t("inline-flex items-center gap-1.5 rounded-full px-2 py-0.5 text-xs font-medium", e.isRunning ? "bg-status-info-bg text-status-info-text" : "bg-status-success-bg text-status-success-text"),
134
- children: [e.isRunning && /* @__PURE__ */ h("span", { className: "h-1.5 w-1.5 rounded-full bg-status-info-text motion-safe:animate-vc-glow-pulse" }), e.isRunning ? w("timecampus.home.recentEntries.status.running", "running") : w("timecampus.home.recentEntries.status.completed", "completed")]
132
+ children: [e.isRunning && /* @__PURE__ */ g("span", { className: "h-1.5 w-1.5 rounded-full bg-status-info-text motion-safe:animate-vc-glow-pulse" }), e.isRunning ? C("timecampus.home.recentEntries.status.running", "running") : C("timecampus.home.recentEntries.status.completed", "completed")]
135
133
  })]
136
134
  })]
137
- }, e.id)), j.length === 0 && /* @__PURE__ */ h("p", {
135
+ }, e.id)), A.length === 0 && /* @__PURE__ */ g("p", {
138
136
  className: "py-4 text-center text-sm text-text-secondary",
139
- children: w("timecampus.home.recentEntries.empty", "No time entries yet")
137
+ children: C("timecampus.home.recentEntries.empty", "No time entries yet")
140
138
  })]
141
139
  })]
142
- }), /* @__PURE__ */ g(y, {
140
+ }), /* @__PURE__ */ _(b, {
143
141
  className: "p-6",
144
- children: [/* @__PURE__ */ g("div", {
142
+ children: [/* @__PURE__ */ _("div", {
145
143
  className: "mb-4",
146
- children: [/* @__PURE__ */ h("h3", {
144
+ children: [/* @__PURE__ */ g("h3", {
147
145
  className: "text-sm font-semibold text-text-primary",
148
- children: w("timecampus.home.summary.title", "Quick Summary")
149
- }), /* @__PURE__ */ h("p", {
146
+ children: C("timecampus.home.summary.title", "Quick Summary")
147
+ }), /* @__PURE__ */ g("p", {
150
148
  className: "text-xs text-text-secondary",
151
- children: w("timecampus.home.summary.description", "Time tracking overview")
149
+ children: C("timecampus.home.summary.description", "Time tracking overview")
152
150
  })]
153
- }), /* @__PURE__ */ g("div", {
151
+ }), /* @__PURE__ */ _("div", {
154
152
  className: "flex flex-col gap-4",
155
153
  children: [
156
- /* @__PURE__ */ g("div", {
154
+ /* @__PURE__ */ _("div", {
157
155
  className: "flex items-center justify-between rounded-lg border border-border-seam bg-bg-surface/40 p-3",
158
- children: [/* @__PURE__ */ g("div", { children: [/* @__PURE__ */ h("p", {
156
+ children: [/* @__PURE__ */ _("div", { children: [/* @__PURE__ */ g("p", {
159
157
  className: "text-xs font-medium text-text-secondary",
160
- children: w("timecampus.home.summary.totalHours", "Total Hours")
161
- }), /* @__PURE__ */ h("p", {
158
+ children: C("timecampus.home.summary.totalHours", "Total Hours")
159
+ }), /* @__PURE__ */ g("p", {
162
160
  className: "mt-1 text-xl font-bold tabular-nums text-text-primary",
163
- children: n(A?.totalHours ?? 0)
164
- })] }), /* @__PURE__ */ h(u, { className: "h-5 w-5 text-text-secondary" })]
161
+ children: n(k?.totalHours ?? 0)
162
+ })] }), /* @__PURE__ */ g(d, { className: "h-5 w-5 text-text-secondary" })]
165
163
  }),
166
- /* @__PURE__ */ g("div", {
164
+ /* @__PURE__ */ _("div", {
167
165
  className: "flex items-center justify-between rounded-lg border border-border-seam bg-bg-surface/40 p-3",
168
- children: [/* @__PURE__ */ g("div", { children: [/* @__PURE__ */ h("p", {
166
+ children: [/* @__PURE__ */ _("div", { children: [/* @__PURE__ */ g("p", {
169
167
  className: "text-xs font-medium text-text-secondary",
170
- children: w("timecampus.home.summary.billableHours", "Billable Hours")
171
- }), /* @__PURE__ */ h("p", {
168
+ children: C("timecampus.home.summary.billableHours", "Billable Hours")
169
+ }), /* @__PURE__ */ g("p", {
172
170
  className: "mt-1 text-xl font-bold tabular-nums text-text-primary",
173
- children: n(A?.billableHours ?? 0)
174
- })] }), /* @__PURE__ */ h(p, { className: "h-5 w-5 text-text-secondary" })]
171
+ children: n(k?.billableHours ?? 0)
172
+ })] }), /* @__PURE__ */ g(m, { className: "h-5 w-5 text-text-secondary" })]
175
173
  }),
176
- /* @__PURE__ */ g("div", {
174
+ /* @__PURE__ */ _("div", {
177
175
  className: "flex items-center justify-between rounded-lg border border-border-seam bg-bg-surface/40 p-3",
178
- children: [/* @__PURE__ */ g("div", { children: [/* @__PURE__ */ h("p", {
176
+ children: [/* @__PURE__ */ _("div", { children: [/* @__PURE__ */ g("p", {
179
177
  className: "text-xs font-medium text-text-secondary",
180
- children: w("timecampus.home.summary.activeTimers", "Active Timers")
181
- }), /* @__PURE__ */ h("p", {
178
+ children: C("timecampus.home.summary.activeTimers", "Active Timers")
179
+ }), /* @__PURE__ */ g("p", {
182
180
  className: "mt-1 text-xl font-bold tabular-nums text-text-primary",
183
- children: A?.activeTimers ?? 0
184
- })] }), /* @__PURE__ */ h(m, { className: "h-5 w-5 text-text-secondary" })]
181
+ children: k?.activeTimers ?? 0
182
+ })] }), /* @__PURE__ */ g(h, { className: "h-5 w-5 text-text-secondary" })]
185
183
  })
186
184
  ]
187
185
  })]
188
186
  })]
189
187
  }),
190
- /* @__PURE__ */ g(b, {
188
+ /* @__PURE__ */ _(x, {
191
189
  treatment: "plain",
192
190
  className: "border-border-seam p-6",
193
- children: [/* @__PURE__ */ g("div", {
191
+ children: [/* @__PURE__ */ _("div", {
194
192
  className: "mb-4",
195
- children: [/* @__PURE__ */ h("h3", {
193
+ children: [/* @__PURE__ */ g("h3", {
196
194
  className: "text-sm font-semibold text-text-primary",
197
- children: w("timecampus.home.quickActions.title", "Quick Actions")
198
- }), /* @__PURE__ */ h("p", {
195
+ children: C("timecampus.home.quickActions.title", "Quick Actions")
196
+ }), /* @__PURE__ */ g("p", {
199
197
  className: "text-xs text-text-secondary",
200
- children: w("timecampus.home.quickActions.description", "Frequently used features")
198
+ children: C("timecampus.home.quickActions.description", "Frequently used features")
201
199
  })]
202
- }), /* @__PURE__ */ h("div", {
200
+ }), /* @__PURE__ */ g("div", {
203
201
  className: "grid grid-cols-2 gap-3 sm:grid-cols-3 lg:grid-cols-6",
204
202
  children: [
205
203
  {
206
- label: w("timecampus.home.quickActions.startTimer.label", "Start Timer"),
207
- icon: f,
204
+ label: C("timecampus.home.quickActions.startTimer.label", "Start Timer"),
205
+ icon: p,
208
206
  route: "/time/timer",
209
- description: w("timecampus.home.quickActions.startTimer.description", "Track your time")
207
+ description: C("timecampus.home.quickActions.startTimer.description", "Track your time")
210
208
  },
211
209
  {
212
- label: w("timecampus.home.quickActions.viewProjects.label", "View Projects"),
213
- icon: d,
210
+ label: C("timecampus.home.quickActions.viewProjects.label", "View Projects"),
211
+ icon: f,
214
212
  route: "/projects",
215
- description: w("timecampus.home.quickActions.viewProjects.description", "Manage projects")
213
+ description: C("timecampus.home.quickActions.viewProjects.description", "Manage projects")
216
214
  },
217
215
  {
218
- label: w("timecampus.home.quickActions.timeEntries.label", "Time Entries"),
219
- icon: u,
216
+ label: C("timecampus.home.quickActions.timeEntries.label", "Time Entries"),
217
+ icon: d,
220
218
  route: "/time/entries",
221
- description: w("timecampus.home.quickActions.timeEntries.description", "View all entries")
219
+ description: C("timecampus.home.quickActions.timeEntries.description", "View all entries")
222
220
  },
223
221
  {
224
- label: w("timecampus.home.quickActions.teamActivity.label", "Team Activity"),
225
- icon: m,
222
+ label: C("timecampus.home.quickActions.teamActivity.label", "Team Activity"),
223
+ icon: h,
226
224
  route: "/team-activity",
227
- description: w("timecampus.home.quickActions.teamActivity.description", "See team status")
225
+ description: C("timecampus.home.quickActions.teamActivity.description", "See team status")
228
226
  },
229
227
  {
230
- label: w("timecampus.home.quickActions.reports.label", "Reports"),
231
- icon: c,
228
+ label: C("timecampus.home.quickActions.reports.label", "Reports"),
229
+ icon: l,
232
230
  route: "/reports",
233
- description: w("timecampus.home.quickActions.reports.description", "Generate reports")
231
+ description: C("timecampus.home.quickActions.reports.description", "Generate reports")
234
232
  },
235
233
  {
236
- label: w("timecampus.home.quickActions.approvals.label", "Approvals"),
237
- icon: l,
234
+ label: C("timecampus.home.quickActions.approvals.label", "Approvals"),
235
+ icon: u,
238
236
  route: "/time-approval",
239
- description: w("timecampus.home.quickActions.approvals.description", "Review timesheets")
237
+ description: C("timecampus.home.quickActions.approvals.description", "Review timesheets")
240
238
  }
241
- ].map(({ label: e, icon: t, description: n, route: r }) => /* @__PURE__ */ g("button", {
242
- onClick: () => T(r),
239
+ ].map(({ label: e, icon: t, description: n, route: r }) => /* @__PURE__ */ _("button", {
240
+ onClick: () => w(r),
243
241
  className: "flex min-h-[2.75rem] flex-col items-center gap-2 rounded-lg border border-border-seam bg-bg-surface p-4 text-center transition-colors hover:bg-bg-sunken/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-action-primary-border",
244
242
  children: [
245
- /* @__PURE__ */ h("div", {
243
+ /* @__PURE__ */ g("div", {
246
244
  className: "rounded-full bg-action-primary-bg/10 p-3",
247
- children: /* @__PURE__ */ h(t, { className: "h-5 w-5 text-primary" })
245
+ children: /* @__PURE__ */ g(t, { className: "h-5 w-5 text-primary" })
248
246
  }),
249
- /* @__PURE__ */ h("span", {
247
+ /* @__PURE__ */ g("span", {
250
248
  className: "text-sm font-medium text-text-primary",
251
249
  children: e
252
250
  }),
253
- /* @__PURE__ */ h("span", {
251
+ /* @__PURE__ */ g("span", {
254
252
  className: "text-xs text-text-secondary",
255
253
  children: n
256
254
  })
@@ -1 +1 @@
1
- {"version":3,"file":"HomePage.js","names":[],"sources":["../../../src/timecampus/pages/HomePage.tsx"],"sourcesContent":["import type { FC } from \"react\";\nimport {\n Clock,\n FolderOpen,\n Users,\n TrendingUp,\n CheckCircle,\n BarChart3,\n Play,\n ArrowRight,\n} from \"lucide-react\";\nimport {\n GlassCard,\n EmphasisPanel,\n AuroraBackground,\n GradientText,\n CTAOverflowMenu,\n} from \"@burdenoff/fe-libs/ui\";\nimport { StatCard } from \"../components/StatCard\";\nimport { LoadingSkeleton } from \"../components/LoadingSkeleton\";\nimport { cn, formatHours } from \"../utils/helpers\";\nimport { useTimeCampusNavigate } from \"../providers/TimeCampusProvider\";\nimport {\n useGetTimeEntryStatsQuery,\n useListTimeEntriesQuery,\n type TimeEntryFieldsFragment,\n} from \"@/generated/wspace-operations\";\nimport { useI18n } from \"@burdenoff/fe-libs/shared/providers/shell/I18nProvider\";\n\nexport const HomePage: FC = () => {\n const { t: translate } = useI18n();\n const t = (key: string, fallback: string): string => {\n const value = translate(key);\n return value === key ? fallback : value;\n };\n const navigate = useTimeCampusNavigate();\n\n const { data: statsData, loading: statsLoading } =\n useGetTimeEntryStatsQuery();\n const { data: entriesData, loading: entriesLoading } =\n useListTimeEntriesQuery({\n variables: { pagination: { limit: 4 } },\n });\n\n const stats = statsData?.timeEntryStats;\n const recentEntries = entriesData?.timeEntries?.items ?? [];\n\n const headerActions = (\n <CTAOverflowMenu\n primary={{\n label: t(\"timecampus.home.actions.startTimer\", \"Start Timer\"),\n onSelect: () => navigate(\"/time/timer\"),\n icon: <Play className=\"h-4 w-4\" />,\n }}\n actions={[\n {\n label: t(\n \"timecampus.home.quickActions.viewProjects.label\",\n \"View Projects\",\n ),\n onSelect: () => navigate(\"/projects\"),\n icon: <FolderOpen className=\"h-4 w-4\" />,\n },\n {\n label: t(\"timecampus.home.quickActions.reports.label\", \"Reports\"),\n onSelect: () => navigate(\"/reports\"),\n icon: <BarChart3 className=\"h-4 w-4\" />,\n },\n ]}\n />\n );\n\n const renderHeader = () => (\n <div className=\"relative overflow-hidden rounded-2xl border border-border-seam bg-gloss-card p-5 sm:p-6\">\n <AuroraBackground intensity=\"subtle\" />\n <div className=\"relative flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between\">\n <div className=\"min-w-0\">\n <h1 className=\"text-2xl font-bold text-text-primary\">\n <GradientText>\n {t(\"timecampus.home.title\", \"Dashboard\")}\n </GradientText>\n </h1>\n <p className=\"mt-1 text-sm text-text-secondary\">\n {t(\n \"timecampus.home.description\",\n \"Overview of your time tracking and productivity\",\n )}\n </p>\n </div>\n {headerActions}\n </div>\n </div>\n );\n\n if (statsLoading && entriesLoading) {\n return (\n <div className=\"min-h-full space-y-6 p-4 sm:p-6\">\n {renderHeader()}\n <LoadingSkeleton variant=\"card\" count={4} />\n </div>\n );\n }\n\n return (\n <div className=\"min-h-full space-y-6 p-4 sm:p-6\">\n {renderHeader()}\n\n {/* Hero metric + supporting metrics */}\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4\">\n <StatCard\n featured\n title={t(\"timecampus.home.stats.today\", \"Today\")}\n value={formatHours(stats?.totalHours ?? 0)}\n icon={<Clock className=\"h-5 w-5\" />}\n trend={{\n value: 12.5,\n label: t(\"timecampus.home.stats.vsYesterday\", \"vs yesterday\"),\n }}\n />\n <StatCard\n title={t(\"timecampus.home.stats.billable\", \"Billable\")}\n value={formatHours(stats?.billableHours ?? 0)}\n icon={<TrendingUp className=\"h-5 w-5\" />}\n description={`${stats?.totalEntries ?? 0} ${t(\"timecampus.home.stats.entries\", \"entries\")}`}\n />\n <StatCard\n title={t(\"timecampus.home.stats.activeTimers\", \"Active Timers\")}\n value={stats?.activeTimers ?? 0}\n icon={<FolderOpen className=\"h-5 w-5\" />}\n description={t(\n \"timecampus.home.stats.currentlyRunning\",\n \"Currently running\",\n )}\n />\n <StatCard\n title={t(\"timecampus.home.stats.totalEntries\", \"Entries\")}\n value={stats?.totalEntries ?? 0}\n icon={<BarChart3 className=\"h-5 w-5\" />}\n description={t(\n \"timecampus.home.stats.totalEntriesDescription\",\n \"Total time entries\",\n )}\n />\n </div>\n\n <div className=\"grid grid-cols-1 gap-6 lg:grid-cols-3\">\n {/* Recent Time Entries */}\n <GlassCard\n treatment=\"plain\"\n className=\"border-border-seam p-6 lg:col-span-2\"\n >\n <div className=\"mb-4 flex items-center justify-between\">\n <div>\n <h3 className=\"text-sm font-semibold text-text-primary\">\n {t(\n \"timecampus.home.recentEntries.title\",\n \"Recent Time Entries\",\n )}\n </h3>\n <p className=\"text-xs text-text-secondary\">\n {t(\n \"timecampus.home.recentEntries.description\",\n \"Your latest tracked activities\",\n )}\n </p>\n </div>\n <button\n onClick={() => navigate(\"/time/entries\")}\n className=\"inline-flex min-h-[2.75rem] items-center gap-1 rounded-md px-2 text-xs font-medium text-primary transition-colors hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-action-primary-border\"\n >\n {t(\"timecampus.home.recentEntries.viewAll\", \"View all\")}{\" \"}\n <ArrowRight className=\"h-3 w-3\" />\n </button>\n </div>\n {entriesLoading ? (\n <LoadingSkeleton variant=\"list\" count={4} />\n ) : (\n <div className=\"flex flex-col gap-3\">\n {recentEntries.map((entry: TimeEntryFieldsFragment) => (\n <div\n key={entry.id}\n className=\"flex items-center justify-between rounded-lg border border-border-seam p-3 transition-colors hover:bg-bg-sunken/50\"\n >\n <div className=\"min-w-0 flex-1\">\n <p className=\"text-sm font-medium text-text-primary\">\n {entry.project?.name ??\n t(\n \"timecampus.home.recentEntries.noProject\",\n \"No Project\",\n )}\n </p>\n <p className=\"text-xs text-text-secondary\">\n {entry.description ??\n t(\n \"timecampus.home.recentEntries.noDescription\",\n \"No description\",\n )}\n </p>\n </div>\n <div className=\"ml-4 text-right\">\n <p className=\"font-mono text-sm font-medium tabular-nums text-text-primary\">\n {formatHours((entry.duration ?? 0) / 3600)}\n </p>\n <span\n className={cn(\n \"inline-flex items-center gap-1.5 rounded-full px-2 py-0.5 text-xs font-medium\",\n entry.isRunning\n ? \"bg-status-info-bg text-status-info-text\"\n : \"bg-status-success-bg text-status-success-text\",\n )}\n >\n {entry.isRunning && (\n <span className=\"h-1.5 w-1.5 rounded-full bg-status-info-text motion-safe:animate-vc-glow-pulse\" />\n )}\n {entry.isRunning\n ? t(\n \"timecampus.home.recentEntries.status.running\",\n \"running\",\n )\n : t(\n \"timecampus.home.recentEntries.status.completed\",\n \"completed\",\n )}\n </span>\n </div>\n </div>\n ))}\n {recentEntries.length === 0 && (\n <p className=\"py-4 text-center text-sm text-text-secondary\">\n {t(\n \"timecampus.home.recentEntries.empty\",\n \"No time entries yet\",\n )}\n </p>\n )}\n </div>\n )}\n </GlassCard>\n\n {/* Quick Summary — the one emphasis zone */}\n <EmphasisPanel className=\"p-6\">\n <div className=\"mb-4\">\n <h3 className=\"text-sm font-semibold text-text-primary\">\n {t(\"timecampus.home.summary.title\", \"Quick Summary\")}\n </h3>\n <p className=\"text-xs text-text-secondary\">\n {t(\n \"timecampus.home.summary.description\",\n \"Time tracking overview\",\n )}\n </p>\n </div>\n <div className=\"flex flex-col gap-4\">\n <div className=\"flex items-center justify-between rounded-lg border border-border-seam bg-bg-surface/40 p-3\">\n <div>\n <p className=\"text-xs font-medium text-text-secondary\">\n {t(\"timecampus.home.summary.totalHours\", \"Total Hours\")}\n </p>\n <p className=\"mt-1 text-xl font-bold tabular-nums text-text-primary\">\n {formatHours(stats?.totalHours ?? 0)}\n </p>\n </div>\n <Clock className=\"h-5 w-5 text-text-secondary\" />\n </div>\n <div className=\"flex items-center justify-between rounded-lg border border-border-seam bg-bg-surface/40 p-3\">\n <div>\n <p className=\"text-xs font-medium text-text-secondary\">\n {t(\"timecampus.home.summary.billableHours\", \"Billable Hours\")}\n </p>\n <p className=\"mt-1 text-xl font-bold tabular-nums text-text-primary\">\n {formatHours(stats?.billableHours ?? 0)}\n </p>\n </div>\n <TrendingUp className=\"h-5 w-5 text-text-secondary\" />\n </div>\n <div className=\"flex items-center justify-between rounded-lg border border-border-seam bg-bg-surface/40 p-3\">\n <div>\n <p className=\"text-xs font-medium text-text-secondary\">\n {t(\"timecampus.home.summary.activeTimers\", \"Active Timers\")}\n </p>\n <p className=\"mt-1 text-xl font-bold tabular-nums text-text-primary\">\n {stats?.activeTimers ?? 0}\n </p>\n </div>\n <Users className=\"h-5 w-5 text-text-secondary\" />\n </div>\n </div>\n </EmphasisPanel>\n </div>\n\n {/* Quick Actions */}\n <GlassCard treatment=\"plain\" className=\"border-border-seam p-6\">\n <div className=\"mb-4\">\n <h3 className=\"text-sm font-semibold text-text-primary\">\n {t(\"timecampus.home.quickActions.title\", \"Quick Actions\")}\n </h3>\n <p className=\"text-xs text-text-secondary\">\n {t(\n \"timecampus.home.quickActions.description\",\n \"Frequently used features\",\n )}\n </p>\n </div>\n <div className=\"grid grid-cols-2 gap-3 sm:grid-cols-3 lg:grid-cols-6\">\n {[\n {\n label: t(\n \"timecampus.home.quickActions.startTimer.label\",\n \"Start Timer\",\n ),\n icon: Play,\n route: \"/time/timer\",\n description: t(\n \"timecampus.home.quickActions.startTimer.description\",\n \"Track your time\",\n ),\n },\n {\n label: t(\n \"timecampus.home.quickActions.viewProjects.label\",\n \"View Projects\",\n ),\n icon: FolderOpen,\n route: \"/projects\",\n description: t(\n \"timecampus.home.quickActions.viewProjects.description\",\n \"Manage projects\",\n ),\n },\n {\n label: t(\n \"timecampus.home.quickActions.timeEntries.label\",\n \"Time Entries\",\n ),\n icon: Clock,\n route: \"/time/entries\",\n description: t(\n \"timecampus.home.quickActions.timeEntries.description\",\n \"View all entries\",\n ),\n },\n {\n label: t(\n \"timecampus.home.quickActions.teamActivity.label\",\n \"Team Activity\",\n ),\n icon: Users,\n route: \"/team-activity\",\n description: t(\n \"timecampus.home.quickActions.teamActivity.description\",\n \"See team status\",\n ),\n },\n {\n label: t(\"timecampus.home.quickActions.reports.label\", \"Reports\"),\n icon: BarChart3,\n route: \"/reports\",\n description: t(\n \"timecampus.home.quickActions.reports.description\",\n \"Generate reports\",\n ),\n },\n {\n label: t(\n \"timecampus.home.quickActions.approvals.label\",\n \"Approvals\",\n ),\n icon: CheckCircle,\n route: \"/time-approval\",\n description: t(\n \"timecampus.home.quickActions.approvals.description\",\n \"Review timesheets\",\n ),\n },\n ].map(({ label, icon: Icon, description, route }) => (\n <button\n key={label}\n onClick={() => navigate(route)}\n className=\"flex min-h-[2.75rem] flex-col items-center gap-2 rounded-lg border border-border-seam bg-bg-surface p-4 text-center transition-colors hover:bg-bg-sunken/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-action-primary-border\"\n >\n <div className=\"rounded-full bg-action-primary-bg/10 p-3\">\n <Icon className=\"h-5 w-5 text-primary\" />\n </div>\n <span className=\"text-sm font-medium text-text-primary\">\n {label}\n </span>\n <span className=\"text-xs text-text-secondary\">{description}</span>\n </button>\n ))}\n </div>\n </GlassCard>\n </div>\n );\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"HomePage.js","names":[],"sources":["../../../src/timecampus/pages/HomePage.tsx"],"sourcesContent":["import type { FC } from \"react\";\nimport {\n Clock,\n FolderOpen,\n Users,\n TrendingUp,\n CheckCircle,\n BarChart3,\n Play,\n ArrowRight,\n} from \"lucide-react\";\nimport {\n GlassCard,\n EmphasisPanel,\n AuroraBackground,\n GradientText,\n CTAOverflowMenu,\n} from \"@burdenoff/fe-libs/ui\";\nimport { StatCard } from \"../components/StatCard\";\nimport { LoadingSkeleton } from \"../components/LoadingSkeleton\";\nimport { cn, formatHours } from \"../utils/helpers\";\nimport { useTimeCampusNavigate } from \"../providers/TimeCampusProvider\";\nimport {\n useGetTimeEntryStatsQuery,\n useListTimeEntriesQuery,\n type TimeEntryFieldsFragment,\n} from \"@/generated/wspace-operations\";\nimport { useTr } from \"../hooks\";\n\nexport const HomePage: FC = () => {\n const t = useTr();\n const navigate = useTimeCampusNavigate();\n\n const { data: statsData, loading: statsLoading } =\n useGetTimeEntryStatsQuery();\n const { data: entriesData, loading: entriesLoading } =\n useListTimeEntriesQuery({\n variables: { pagination: { limit: 4 } },\n });\n\n const stats = statsData?.timeEntryStats;\n const recentEntries = entriesData?.timeEntries?.items ?? [];\n\n const headerActions = (\n <CTAOverflowMenu\n primary={{\n label: t(\"timecampus.home.actions.startTimer\", \"Start Timer\"),\n onSelect: () => navigate(\"/time/timer\"),\n icon: <Play className=\"h-4 w-4\" />,\n }}\n actions={[\n {\n label: t(\n \"timecampus.home.quickActions.viewProjects.label\",\n \"View Projects\",\n ),\n onSelect: () => navigate(\"/projects\"),\n icon: <FolderOpen className=\"h-4 w-4\" />,\n },\n {\n label: t(\"timecampus.home.quickActions.reports.label\", \"Reports\"),\n onSelect: () => navigate(\"/reports\"),\n icon: <BarChart3 className=\"h-4 w-4\" />,\n },\n ]}\n />\n );\n\n const renderHeader = () => (\n <div className=\"relative overflow-hidden rounded-2xl border border-border-seam bg-gloss-card p-5 sm:p-6\">\n <AuroraBackground intensity=\"subtle\" />\n <div className=\"relative flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between\">\n <div className=\"min-w-0\">\n <h1 className=\"text-2xl font-bold text-text-primary\">\n <GradientText>\n {t(\"timecampus.home.title\", \"Dashboard\")}\n </GradientText>\n </h1>\n <p className=\"mt-1 text-sm text-text-secondary\">\n {t(\n \"timecampus.home.description\",\n \"Overview of your time tracking and productivity\",\n )}\n </p>\n </div>\n {headerActions}\n </div>\n </div>\n );\n\n if (statsLoading && entriesLoading) {\n return (\n <div className=\"min-h-full space-y-6 p-4 sm:p-6\">\n {renderHeader()}\n <LoadingSkeleton variant=\"card\" count={4} />\n </div>\n );\n }\n\n return (\n <div className=\"min-h-full space-y-6 p-4 sm:p-6\">\n {renderHeader()}\n\n {/* Hero metric + supporting metrics */}\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4\">\n <StatCard\n featured\n title={t(\"timecampus.home.stats.today\", \"Today\")}\n value={formatHours(stats?.totalHours ?? 0)}\n icon={<Clock className=\"h-5 w-5\" />}\n trend={{\n value: 12.5,\n label: t(\"timecampus.home.stats.vsYesterday\", \"vs yesterday\"),\n }}\n />\n <StatCard\n title={t(\"timecampus.home.stats.billable\", \"Billable\")}\n value={formatHours(stats?.billableHours ?? 0)}\n icon={<TrendingUp className=\"h-5 w-5\" />}\n description={`${stats?.totalEntries ?? 0} ${t(\"timecampus.home.stats.entries\", \"entries\")}`}\n />\n <StatCard\n title={t(\"timecampus.home.stats.activeTimers\", \"Active Timers\")}\n value={stats?.activeTimers ?? 0}\n icon={<FolderOpen className=\"h-5 w-5\" />}\n description={t(\n \"timecampus.home.stats.currentlyRunning\",\n \"Currently running\",\n )}\n />\n <StatCard\n title={t(\"timecampus.home.stats.totalEntries\", \"Entries\")}\n value={stats?.totalEntries ?? 0}\n icon={<BarChart3 className=\"h-5 w-5\" />}\n description={t(\n \"timecampus.home.stats.totalEntriesDescription\",\n \"Total time entries\",\n )}\n />\n </div>\n\n <div className=\"grid grid-cols-1 gap-6 lg:grid-cols-3\">\n {/* Recent Time Entries */}\n <GlassCard\n treatment=\"plain\"\n className=\"border-border-seam p-6 lg:col-span-2\"\n >\n <div className=\"mb-4 flex items-center justify-between\">\n <div>\n <h3 className=\"text-sm font-semibold text-text-primary\">\n {t(\n \"timecampus.home.recentEntries.title\",\n \"Recent Time Entries\",\n )}\n </h3>\n <p className=\"text-xs text-text-secondary\">\n {t(\n \"timecampus.home.recentEntries.description\",\n \"Your latest tracked activities\",\n )}\n </p>\n </div>\n <button\n onClick={() => navigate(\"/time/entries\")}\n className=\"inline-flex min-h-[2.75rem] items-center gap-1 rounded-md px-2 text-xs font-medium text-primary transition-colors hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-action-primary-border\"\n >\n {t(\"timecampus.home.recentEntries.viewAll\", \"View all\")}{\" \"}\n <ArrowRight className=\"h-3 w-3\" />\n </button>\n </div>\n {entriesLoading ? (\n <LoadingSkeleton variant=\"list\" count={4} />\n ) : (\n <div className=\"flex flex-col gap-3\">\n {recentEntries.map((entry: TimeEntryFieldsFragment) => (\n <div\n key={entry.id}\n className=\"flex items-center justify-between rounded-lg border border-border-seam p-3 transition-colors hover:bg-bg-sunken/50\"\n >\n <div className=\"min-w-0 flex-1\">\n <p className=\"text-sm font-medium text-text-primary\">\n {entry.project?.name ??\n t(\n \"timecampus.home.recentEntries.noProject\",\n \"No Project\",\n )}\n </p>\n <p className=\"text-xs text-text-secondary\">\n {entry.description ??\n t(\n \"timecampus.home.recentEntries.noDescription\",\n \"No description\",\n )}\n </p>\n </div>\n <div className=\"ml-4 text-right\">\n <p className=\"font-mono text-sm font-medium tabular-nums text-text-primary\">\n {formatHours((entry.duration ?? 0) / 3600)}\n </p>\n <span\n className={cn(\n \"inline-flex items-center gap-1.5 rounded-full px-2 py-0.5 text-xs font-medium\",\n entry.isRunning\n ? \"bg-status-info-bg text-status-info-text\"\n : \"bg-status-success-bg text-status-success-text\",\n )}\n >\n {entry.isRunning && (\n <span className=\"h-1.5 w-1.5 rounded-full bg-status-info-text motion-safe:animate-vc-glow-pulse\" />\n )}\n {entry.isRunning\n ? t(\n \"timecampus.home.recentEntries.status.running\",\n \"running\",\n )\n : t(\n \"timecampus.home.recentEntries.status.completed\",\n \"completed\",\n )}\n </span>\n </div>\n </div>\n ))}\n {recentEntries.length === 0 && (\n <p className=\"py-4 text-center text-sm text-text-secondary\">\n {t(\n \"timecampus.home.recentEntries.empty\",\n \"No time entries yet\",\n )}\n </p>\n )}\n </div>\n )}\n </GlassCard>\n\n {/* Quick Summary — the one emphasis zone */}\n <EmphasisPanel className=\"p-6\">\n <div className=\"mb-4\">\n <h3 className=\"text-sm font-semibold text-text-primary\">\n {t(\"timecampus.home.summary.title\", \"Quick Summary\")}\n </h3>\n <p className=\"text-xs text-text-secondary\">\n {t(\n \"timecampus.home.summary.description\",\n \"Time tracking overview\",\n )}\n </p>\n </div>\n <div className=\"flex flex-col gap-4\">\n <div className=\"flex items-center justify-between rounded-lg border border-border-seam bg-bg-surface/40 p-3\">\n <div>\n <p className=\"text-xs font-medium text-text-secondary\">\n {t(\"timecampus.home.summary.totalHours\", \"Total Hours\")}\n </p>\n <p className=\"mt-1 text-xl font-bold tabular-nums text-text-primary\">\n {formatHours(stats?.totalHours ?? 0)}\n </p>\n </div>\n <Clock className=\"h-5 w-5 text-text-secondary\" />\n </div>\n <div className=\"flex items-center justify-between rounded-lg border border-border-seam bg-bg-surface/40 p-3\">\n <div>\n <p className=\"text-xs font-medium text-text-secondary\">\n {t(\"timecampus.home.summary.billableHours\", \"Billable Hours\")}\n </p>\n <p className=\"mt-1 text-xl font-bold tabular-nums text-text-primary\">\n {formatHours(stats?.billableHours ?? 0)}\n </p>\n </div>\n <TrendingUp className=\"h-5 w-5 text-text-secondary\" />\n </div>\n <div className=\"flex items-center justify-between rounded-lg border border-border-seam bg-bg-surface/40 p-3\">\n <div>\n <p className=\"text-xs font-medium text-text-secondary\">\n {t(\"timecampus.home.summary.activeTimers\", \"Active Timers\")}\n </p>\n <p className=\"mt-1 text-xl font-bold tabular-nums text-text-primary\">\n {stats?.activeTimers ?? 0}\n </p>\n </div>\n <Users className=\"h-5 w-5 text-text-secondary\" />\n </div>\n </div>\n </EmphasisPanel>\n </div>\n\n {/* Quick Actions */}\n <GlassCard treatment=\"plain\" className=\"border-border-seam p-6\">\n <div className=\"mb-4\">\n <h3 className=\"text-sm font-semibold text-text-primary\">\n {t(\"timecampus.home.quickActions.title\", \"Quick Actions\")}\n </h3>\n <p className=\"text-xs text-text-secondary\">\n {t(\n \"timecampus.home.quickActions.description\",\n \"Frequently used features\",\n )}\n </p>\n </div>\n <div className=\"grid grid-cols-2 gap-3 sm:grid-cols-3 lg:grid-cols-6\">\n {[\n {\n label: t(\n \"timecampus.home.quickActions.startTimer.label\",\n \"Start Timer\",\n ),\n icon: Play,\n route: \"/time/timer\",\n description: t(\n \"timecampus.home.quickActions.startTimer.description\",\n \"Track your time\",\n ),\n },\n {\n label: t(\n \"timecampus.home.quickActions.viewProjects.label\",\n \"View Projects\",\n ),\n icon: FolderOpen,\n route: \"/projects\",\n description: t(\n \"timecampus.home.quickActions.viewProjects.description\",\n \"Manage projects\",\n ),\n },\n {\n label: t(\n \"timecampus.home.quickActions.timeEntries.label\",\n \"Time Entries\",\n ),\n icon: Clock,\n route: \"/time/entries\",\n description: t(\n \"timecampus.home.quickActions.timeEntries.description\",\n \"View all entries\",\n ),\n },\n {\n label: t(\n \"timecampus.home.quickActions.teamActivity.label\",\n \"Team Activity\",\n ),\n icon: Users,\n route: \"/team-activity\",\n description: t(\n \"timecampus.home.quickActions.teamActivity.description\",\n \"See team status\",\n ),\n },\n {\n label: t(\"timecampus.home.quickActions.reports.label\", \"Reports\"),\n icon: BarChart3,\n route: \"/reports\",\n description: t(\n \"timecampus.home.quickActions.reports.description\",\n \"Generate reports\",\n ),\n },\n {\n label: t(\n \"timecampus.home.quickActions.approvals.label\",\n \"Approvals\",\n ),\n icon: CheckCircle,\n route: \"/time-approval\",\n description: t(\n \"timecampus.home.quickActions.approvals.description\",\n \"Review timesheets\",\n ),\n },\n ].map(({ label, icon: Icon, description, route }) => (\n <button\n key={label}\n onClick={() => navigate(route)}\n className=\"flex min-h-[2.75rem] flex-col items-center gap-2 rounded-lg border border-border-seam bg-bg-surface p-4 text-center transition-colors hover:bg-bg-sunken/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-action-primary-border\"\n >\n <div className=\"rounded-full bg-action-primary-bg/10 p-3\">\n <Icon className=\"h-5 w-5 text-primary\" />\n </div>\n <span className=\"text-sm font-medium text-text-primary\">\n {label}\n </span>\n <span className=\"text-xs text-text-secondary\">{description}</span>\n </button>\n ))}\n </div>\n </GlassCard>\n </div>\n );\n};\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"TimerPage.d.ts","sourceRoot":"","sources":["../../../../src/timecampus/pages/time/TimerPage.tsx"],"names":[],"mappings":"AA2BA,wBAAgB,SAAS,gCA+UxB"}
1
+ {"version":3,"file":"TimerPage.d.ts","sourceRoot":"","sources":["../../../../src/timecampus/pages/time/TimerPage.tsx"],"names":[],"mappings":"AA4BA,wBAAgB,SAAS,gCAqaxB"}
@@ -1,11 +1,13 @@
1
1
  import { useTimeCampusNavigate as e } from "../../providers/TimeCampusProvider.js";
2
- import { formatTimerDuration as t, getInitials as ee } from "../../utils/helpers.js";
3
- import { LoadingSkeleton as n } from "../../components/LoadingSkeleton.js";
4
- import { GetTimeEntryStatsDocument as te, ListRunningTimersDocument as r, ListTimeEntriesDocument as i, useDeleteTimeEntryMutation as ne, useListProjectsQuery as a, useListRunningTimersQuery as o, useStartTimerMutation as s, useStopTimerMutation as c } from "../../../generated/wspace-operations.js";
5
- import { PageLayout as l } from "../../components/PageLayout.js";
6
- import { useTimeCampusStore as u } from "../../store/timecampusStore.js";
7
- import { StatusBadge as d } from "../../components/StatusBadge.js";
8
- import { EmptyState as f } from "../../components/EmptyState.js";
2
+ import { formatTimerDuration as t, getInitials as n } from "../../utils/helpers.js";
3
+ import { LoadingSkeleton as r } from "../../components/LoadingSkeleton.js";
4
+ import { GetTimeEntryStatsDocument as ee, ListRunningTimersDocument as i, ListTimeEntriesDocument as a, useDeleteTimeEntryMutation as o, useListProjectsQuery as s, useListRunningTimersQuery as c, useStartTimerMutation as l, useStopTimerMutation as u } from "../../../generated/wspace-operations.js";
5
+ import { useTr as d } from "../../shared/hooks/useTr.js";
6
+ import "../../hooks/index.js";
7
+ import { PageLayout as f } from "../../components/PageLayout.js";
8
+ import { useTimeCampusStore as te } from "../../store/timecampusStore.js";
9
+ import { StatusBadge as ne } from "../../components/StatusBadge.js";
10
+ import { EmptyState as re } from "../../components/EmptyState.js";
9
11
  import { Modal as p } from "../../components/Modal.js";
10
12
  import { useEffect as m, useState as h } from "react";
11
13
  import { Play as g, RotateCcw as _, Square as v } from "lucide-react";
@@ -13,14 +15,14 @@ import { Fragment as y, jsx as b, jsxs as x } from "react/jsx-runtime";
13
15
  import { GlassCard as S, NextSteps as C } from "@burdenoff/fe-libs/ui";
14
16
  //#region src/timecampus/pages/time/TimerPage.tsx
15
17
  function w() {
16
- let { activeTimer: w, tickTimer: T, syncTimerFromServer: E } = u(), D = e(), [O, k] = h(!1), [A, j] = h(""), [M, N] = h(""), [P, F] = h(!1), [I, L] = h(null), { data: R, loading: z, refetch: B } = o({ fetchPolicy: "cache-and-network" }), { data: V } = a({ variables: { pagination: { limit: 50 } } }), H = V?.projects?.items ?? [], U = R?.runningTimers ?? [], W = U[0] ?? null, G = [
17
- { query: r },
18
+ let { activeTimer: w, tickTimer: T, syncTimerFromServer: E } = te(), D = e(), O = d(), [ie, k] = h(!1), [A, j] = h(""), [M, N] = h(""), [P, F] = h(!1), [I, L] = h(null), { data: R, loading: z, refetch: B } = c({ fetchPolicy: "cache-and-network" }), { data: V } = s({ variables: { pagination: { limit: 50 } } }), H = V?.projects?.items ?? [], U = R?.runningTimers ?? [], W = U[0] ?? null, G = [
19
+ { query: i },
18
20
  {
19
- query: i,
21
+ query: a,
20
22
  variables: { pagination: { limit: 50 } }
21
23
  },
22
- { query: te }
23
- ], [K, { loading: q, error: J }] = s({ refetchQueries: G }), [Y, { loading: X }] = c({ refetchQueries: G }), [re, { loading: Z }] = ne({ refetchQueries: G });
24
+ { query: ee }
25
+ ], [K, { loading: q, error: J }] = l({ refetchQueries: G }), [Y, { loading: X }] = u({ refetchQueries: G }), [ae, { loading: Z }] = o({ refetchQueries: G });
24
26
  m(() => {
25
27
  W ? E({
26
28
  timeEntryId: W.timeEntry.id,
@@ -38,7 +40,7 @@ function w() {
38
40
  let e = setInterval(() => T(), 1e3);
39
41
  return () => clearInterval(e);
40
42
  }, [w?.isRunning, T]);
41
- let ie = async () => {
43
+ let oe = async () => {
42
44
  await K({ variables: { input: {
43
45
  description: A.trim() || void 0,
44
46
  projectId: M || void 0,
@@ -47,13 +49,13 @@ function w() {
47
49
  } } }), k(!1), j(""), N(""), F(!1), await B();
48
50
  }, Q = async () => {
49
51
  W && (await Y({ variables: { id: W.timeEntry.id } }), await B());
50
- }, ae = async () => {
51
- I && (await Y({ variables: { id: I.timeEntry.id } }), await re({ variables: { id: I.timeEntry.id } }), L(null), await B());
52
+ }, se = async () => {
53
+ I && (await Y({ variables: { id: I.timeEntry.id } }), await ae({ variables: { id: I.timeEntry.id } }), L(null), await B());
52
54
  }, $ = !!W;
53
- return /* @__PURE__ */ x(l, {
54
- title: "Timer",
55
- description: "Focus timer for deep work sessions",
56
- purpose: "A single, distraction-free stopwatch for the task in front of you — start it when you begin focused work and it logs the session as a time entry you can attribute to a project later.",
55
+ return /* @__PURE__ */ x(f, {
56
+ title: O("timecampus.timer.title", "Timer"),
57
+ description: O("timecampus.timer.description", "Focus timer for deep work sessions"),
58
+ purpose: O("timecampus.timer.purpose", "A single, distraction-free stopwatch for the task in front of you — start it when you begin focused work and it logs the session as a time entry you can attribute to a project later."),
57
59
  children: [
58
60
  /* @__PURE__ */ x("div", {
59
61
  className: "mx-auto max-w-lg",
@@ -64,20 +66,20 @@ function w() {
64
66
  steps: [
65
67
  {
66
68
  id: "start",
67
- label: $ ? "Stop the running timer" : "Start the timer",
68
- description: $ ? "End this session and save it as a time entry." : "Begin tracking the task you're working on right now.",
69
+ label: $ ? O("timecampus.timer.nextSteps.stopRunning", "Stop the running timer") : O("timecampus.timer.nextSteps.start", "Start the timer"),
70
+ description: $ ? O("timecampus.timer.nextSteps.stopDescription", "End this session and save it as a time entry.") : O("timecampus.timer.nextSteps.startDescription", "Begin tracking the task you're working on right now."),
69
71
  onClick: () => $ ? Q() : k(!0)
70
72
  },
71
73
  {
72
74
  id: "assign-project",
73
- label: "Assign work to a project",
74
- description: "Pick a project so this time bills correctly.",
75
+ label: O("timecampus.timer.nextSteps.assignProject", "Assign work to a project"),
76
+ description: O("timecampus.timer.nextSteps.assignDescription", "Pick a project so this time bills correctly."),
75
77
  onClick: () => D("/projects")
76
78
  },
77
79
  {
78
80
  id: "review-entries",
79
- label: "Review your time entries",
80
- description: "Check and tidy everything you've logged.",
81
+ label: O("timecampus.timer.nextSteps.review", "Review your time entries"),
82
+ description: O("timecampus.timer.nextSteps.reviewDescription", "Check and tidy everything you've logged."),
81
83
  onClick: () => D("/time/entries")
82
84
  }
83
85
  ]
@@ -89,7 +91,7 @@ function w() {
89
91
  children: [
90
92
  /* @__PURE__ */ b("p", {
91
93
  className: "font-mono text-5xl font-bold tabular-nums text-text-primary sm:text-6xl",
92
- children: w ? t(w.seconds) : "00:00:00"
94
+ children: w ? t(w.seconds) : O("timecampus.timer.elapsedDefault", "00:00:00")
93
95
  }),
94
96
  w?.description && /* @__PURE__ */ b("p", {
95
97
  className: "mt-2 text-sm text-text-secondary",
@@ -101,18 +103,18 @@ function w() {
101
103
  onClick: Q,
102
104
  disabled: X,
103
105
  className: "inline-flex items-center gap-2 rounded-md bg-action-danger-bg px-6 py-3 text-sm font-medium text-action-primary-text hover:bg-action-danger-bg/90 disabled:opacity-50",
104
- children: [/* @__PURE__ */ b(v, { className: "h-4 w-4" }), X ? "Stopping..." : "Stop"]
106
+ children: [/* @__PURE__ */ b(v, { className: "h-4 w-4" }), X ? O("timecampus.timer.stopping", "Stopping...") : O("timecampus.timer.stop", "Stop")]
105
107
  }) : /* @__PURE__ */ x("button", {
106
108
  onClick: () => k(!0),
107
109
  disabled: q,
108
110
  className: "inline-flex items-center gap-2 rounded-md bg-action-primary-bg px-6 py-3 text-sm font-medium text-action-primary-text hover:bg-action-primary-bg/90 disabled:opacity-50",
109
- children: [/* @__PURE__ */ b(g, { className: "h-4 w-4" }), "Start"]
111
+ children: [/* @__PURE__ */ b(g, { className: "h-4 w-4" }), O("timecampus.timer.start", "Start")]
110
112
  }), /* @__PURE__ */ x("button", {
111
113
  onClick: () => W && L(W),
112
114
  disabled: !$ || Z,
113
115
  className: "inline-flex items-center gap-2 rounded-md border border-border-subtle px-4 py-3 text-sm font-medium text-text-primary hover:bg-bg-sunken/50 disabled:opacity-50",
114
- title: "Discard the running timer without saving",
115
- children: [/* @__PURE__ */ b(_, { className: "h-4 w-4" }), "Reset"]
116
+ title: O("timecampus.timer.resetTooltip", "Discard the running timer without saving"),
117
+ children: [/* @__PURE__ */ b(_, { className: "h-4 w-4" }), O("timecampus.timer.reset", "Reset")]
116
118
  })]
117
119
  })
118
120
  ]
@@ -124,19 +126,19 @@ function w() {
124
126
  className: "mb-4 flex items-center justify-between",
125
127
  children: [/* @__PURE__ */ b("h3", {
126
128
  className: "text-sm font-semibold text-text-primary",
127
- children: "Running Timers"
128
- }), U.length > 0 && /* @__PURE__ */ b(d, {
129
- label: `${U.length} active`,
129
+ children: O("timecampus.timer.runningTimers.title", "Running Timers")
130
+ }), U.length > 0 && /* @__PURE__ */ b(ne, {
131
+ label: O("timecampus.timer.runningTimers.active", "{{count}} active", { count: U.length }),
130
132
  variant: "success",
131
133
  dot: !0
132
134
  })]
133
- }), z && U.length === 0 ? /* @__PURE__ */ b(n, {
135
+ }), z && U.length === 0 ? /* @__PURE__ */ b(r, {
134
136
  variant: "list",
135
137
  count: 2
136
- }) : U.length === 0 ? /* @__PURE__ */ b(f, {
138
+ }) : U.length === 0 ? /* @__PURE__ */ b(re, {
137
139
  illustration: "empty-sessions",
138
- title: "No active timers on the server",
139
- description: "Timers you start sync here so you can pick them up on any device."
140
+ title: O("timecampus.timer.runningTimers.emptyTitle", "No active timers on the server"),
141
+ description: O("timecampus.timer.runningTimers.emptyDescription", "Timers you start sync here so you can pick them up on any device.")
140
142
  }) : /* @__PURE__ */ b("div", {
141
143
  className: "flex flex-col gap-2",
142
144
  children: U.map((e) => /* @__PURE__ */ x("div", {
@@ -144,16 +146,16 @@ function w() {
144
146
  children: [
145
147
  /* @__PURE__ */ b("span", {
146
148
  className: "flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-action-primary-bg/10 text-xs font-semibold text-primary",
147
- children: ee(e.timeEntry.project?.name ?? "No Project")
149
+ children: n(e.timeEntry.project?.name ?? O("timecampus.timer.runningTimers.noProject", "No Project"))
148
150
  }),
149
151
  /* @__PURE__ */ x("div", {
150
152
  className: "min-w-0 flex-1",
151
153
  children: [/* @__PURE__ */ b("p", {
152
154
  className: "truncate text-sm font-medium text-text-primary",
153
- children: e.timeEntry.project?.name ?? "No Project"
155
+ children: e.timeEntry.project?.name ?? O("timecampus.timer.runningTimers.noProject", "No Project")
154
156
  }), /* @__PURE__ */ b("p", {
155
157
  className: "truncate text-xs text-text-secondary",
156
- children: e.timeEntry.description ?? "No description"
158
+ children: e.timeEntry.description ?? O("timecampus.timer.runningTimers.noDescription", "No description")
157
159
  })]
158
160
  }),
159
161
  /* @__PURE__ */ b("p", {
@@ -167,46 +169,46 @@ function w() {
167
169
  ]
168
170
  }),
169
171
  /* @__PURE__ */ x(p, {
170
- open: O,
172
+ open: ie,
171
173
  onClose: () => k(!1),
172
- title: "Start a timer",
173
- description: "Track the task you're working on right now.",
174
+ title: O("timecampus.timer.modal.startTitle", "Start a timer"),
175
+ description: O("timecampus.timer.modal.startDescription", "Track the task you're working on right now."),
174
176
  footer: /* @__PURE__ */ x(y, { children: [/* @__PURE__ */ b("button", {
175
177
  onClick: () => k(!1),
176
178
  className: "rounded-md border border-border-subtle px-4 py-2 text-sm font-medium text-text-primary hover:bg-bg-sunken/50",
177
- children: "Cancel"
179
+ children: O("timecampus.timer.modal.cancel", "Cancel")
178
180
  }), /* @__PURE__ */ b("button", {
179
- onClick: ie,
181
+ onClick: oe,
180
182
  disabled: q,
181
183
  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",
182
- children: q ? "Starting..." : "Start Timer"
184
+ children: q ? O("timecampus.timer.modal.starting", "Starting...") : O("timecampus.timer.modal.startTimer", "Start Timer")
183
185
  })] }),
184
186
  children: [J && /* @__PURE__ */ b("div", {
185
187
  className: "mb-4 rounded-lg border border-status-error-bg/40 bg-status-error-bg p-3 text-sm text-status-error-text",
186
- children: "Failed to start the timer. Please try again."
188
+ children: O("timecampus.timer.modal.startError", "Failed to start the timer. Please try again.")
187
189
  }), /* @__PURE__ */ x("div", {
188
190
  className: "space-y-4",
189
191
  children: [
190
192
  /* @__PURE__ */ x("div", { children: [/* @__PURE__ */ b("label", {
191
193
  className: "block text-sm font-medium text-text-primary",
192
- children: "What are you working on?"
194
+ children: O("timecampus.timer.modal.descriptionLabel", "What are you working on?")
193
195
  }), /* @__PURE__ */ b("input", {
194
196
  type: "text",
195
- placeholder: "Description",
197
+ placeholder: O("timecampus.timer.modal.descriptionPlaceholder", "Description"),
196
198
  value: A,
197
199
  onChange: (e) => j(e.target.value),
198
200
  className: "mt-1 w-full rounded-md border border-border-subtle bg-bg-surface px-3 py-2 text-sm text-text-primary placeholder:text-text-secondary"
199
201
  })] }),
200
202
  /* @__PURE__ */ x("div", { children: [/* @__PURE__ */ b("label", {
201
203
  className: "block text-sm font-medium text-text-primary",
202
- children: "Project"
204
+ children: O("timecampus.timer.modal.projectLabel", "Project")
203
205
  }), /* @__PURE__ */ x("select", {
204
206
  value: M,
205
207
  onChange: (e) => N(e.target.value),
206
208
  className: "mt-1 w-full rounded-md border border-border-subtle bg-bg-surface px-3 py-2 text-sm text-text-primary",
207
209
  children: [/* @__PURE__ */ b("option", {
208
210
  value: "",
209
- children: "No Project"
211
+ children: O("timecampus.timer.modal.noProject", "No Project")
210
212
  }), H.map((e) => /* @__PURE__ */ b("option", {
211
213
  value: e.id,
212
214
  children: e.name
@@ -219,7 +221,7 @@ function w() {
219
221
  checked: P,
220
222
  onChange: (e) => F(e.target.checked),
221
223
  className: "h-4 w-4 rounded border-border-subtle"
222
- }), "Billable"]
224
+ }), O("timecampus.timer.modal.billable", "Billable")]
223
225
  })
224
226
  ]
225
227
  })]
@@ -227,29 +229,29 @@ function w() {
227
229
  /* @__PURE__ */ b(p, {
228
230
  open: !!I,
229
231
  onClose: () => L(null),
230
- title: "Discard running timer?",
231
- description: "This stops the timer and deletes the in-progress entry. It cannot be undone.",
232
+ title: O("timecampus.timer.modal.discardTitle", "Discard running timer?"),
233
+ description: O("timecampus.timer.modal.discardDescription", "This stops the timer and deletes the in-progress entry. It cannot be undone."),
232
234
  footer: /* @__PURE__ */ x(y, { children: [/* @__PURE__ */ b("button", {
233
235
  onClick: () => L(null),
234
236
  className: "rounded-md border border-border-subtle px-4 py-2 text-sm font-medium text-text-primary hover:bg-bg-sunken/50",
235
- children: "Cancel"
237
+ children: O("timecampus.timer.modal.cancel", "Cancel")
236
238
  }), /* @__PURE__ */ b("button", {
237
- onClick: ae,
239
+ onClick: se,
238
240
  disabled: X || Z,
239
241
  className: "rounded-md bg-action-danger-bg px-4 py-2 text-sm font-medium text-action-primary-text hover:bg-action-danger-bg/90 disabled:opacity-50",
240
- children: X || Z ? "Discarding..." : "Discard"
242
+ children: X || Z ? O("timecampus.timer.modal.discarding", "Discarding...") : O("timecampus.timer.modal.discard", "Discard")
241
243
  })] }),
242
244
  children: /* @__PURE__ */ x("p", {
243
245
  className: "text-sm text-text-secondary",
244
246
  children: [
245
- "The timer for",
247
+ O("timecampus.timer.modal.discardMessagePrefix", "The timer for"),
246
248
  " ",
247
249
  /* @__PURE__ */ b("span", {
248
250
  className: "font-medium text-text-primary",
249
- children: I?.timeEntry.project?.name ?? I?.timeEntry.description ?? "this session"
251
+ children: I?.timeEntry.project?.name ?? I?.timeEntry.description ?? O("timecampus.timer.modal.sessionFallback", "this session")
250
252
  }),
251
253
  " ",
252
- "will be discarded."
254
+ O("timecampus.timer.modal.discardMessageSuffix", "will be discarded.")
253
255
  ]
254
256
  })
255
257
  })
@@ -1 +1 @@
1
- {"version":3,"file":"TimerPage.js","names":[],"sources":["../../../../src/timecampus/pages/time/TimerPage.tsx"],"sourcesContent":["import { useEffect, useState } from \"react\";\nimport { Play, Square, RotateCcw } from \"lucide-react\";\nimport { GlassCard, NextSteps } from \"@burdenoff/fe-libs/ui\";\nimport { PageLayout } from \"../../components/PageLayout\";\nimport { Modal } from \"../../components/Modal\";\nimport { EmptyState } from \"../../components/EmptyState\";\nimport { LoadingSkeleton } from \"../../components/LoadingSkeleton\";\nimport { StatusBadge } from \"../../components/StatusBadge\";\nimport { useTimeCampusNavigate } from \"../../providers/TimeCampusProvider\";\nimport { formatTimerDuration, getInitials } from \"../../utils/helpers\";\nimport { useTimeCampusStore } from \"../../store/timecampusStore\";\nimport {\n useListRunningTimersQuery,\n useStartTimerMutation,\n useStopTimerMutation,\n useDeleteTimeEntryMutation,\n useListProjectsQuery,\n ListRunningTimersDocument,\n ListTimeEntriesDocument,\n GetTimeEntryStatsDocument,\n type ListRunningTimersQuery,\n type ListProjectsQuery,\n} from \"@/generated/wspace-operations\";\n\ntype RunningTimerItem = ListRunningTimersQuery[\"runningTimers\"][number];\ntype ProjectOption = ListProjectsQuery[\"projects\"][\"items\"][number];\n\nexport function TimerPage() {\n const { activeTimer, tickTimer, syncTimerFromServer } = useTimeCampusStore();\n const navigate = useTimeCampusNavigate();\n\n const [startOpen, setStartOpen] = useState(false);\n const [description, setDescription] = useState(\"\");\n const [projectId, setProjectId] = useState(\"\");\n const [billable, setBillable] = useState(false);\n const [resetTarget, setResetTarget] = useState<RunningTimerItem | null>(null);\n\n const {\n data: runningData,\n loading: runningLoading,\n refetch: refetchRunning,\n } = useListRunningTimersQuery({ fetchPolicy: \"cache-and-network\" });\n\n const { data: projectsData } = useListProjectsQuery({\n variables: { pagination: { limit: 50 } },\n });\n const projects: ProjectOption[] = projectsData?.projects?.items ?? [];\n\n const runningTimers = runningData?.runningTimers ?? [];\n const serverTimer = runningTimers[0] ?? null;\n\n const refetchAll = [\n { query: ListRunningTimersDocument },\n {\n query: ListTimeEntriesDocument,\n variables: { pagination: { limit: 50 } },\n },\n { query: GetTimeEntryStatsDocument },\n ];\n\n const [startTimerMutation, { loading: starting, error: startError }] =\n useStartTimerMutation({ refetchQueries: refetchAll });\n const [stopTimerMutation, { loading: stopping }] = useStopTimerMutation({\n refetchQueries: refetchAll,\n });\n const [deleteTimeEntry, { loading: deleting }] = useDeleteTimeEntryMutation({\n refetchQueries: refetchAll,\n });\n\n // Backend is the source of truth for whether a timer is running — mirror the\n // server running timer into the local display store whenever it changes.\n useEffect(() => {\n if (serverTimer) {\n syncTimerFromServer({\n timeEntryId: serverTimer.timeEntry.id,\n elapsedSeconds: serverTimer.elapsedSeconds,\n projectId: serverTimer.timeEntry.projectId ?? undefined,\n taskId: serverTimer.timeEntry.taskId ?? undefined,\n description: serverTimer.timeEntry.description ?? undefined,\n });\n } else if (!runningLoading) {\n syncTimerFromServer(null);\n }\n // Only re-sync when the server's running timer identity/elapsed changes.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [serverTimer?.timeEntry.id, serverTimer?.elapsedSeconds, runningLoading]);\n\n // Local ticking display for the live seconds counter.\n useEffect(() => {\n if (!activeTimer?.isRunning) return;\n const interval = setInterval(() => tickTimer(), 1000);\n return () => clearInterval(interval);\n }, [activeTimer?.isRunning, tickTimer]);\n\n const handleStart = async () => {\n await startTimerMutation({\n variables: {\n input: {\n description: description.trim() || undefined,\n projectId: projectId || undefined,\n billable,\n source: \"timer\",\n },\n },\n });\n setStartOpen(false);\n setDescription(\"\");\n setProjectId(\"\");\n setBillable(false);\n await refetchRunning();\n };\n\n const handleStop = async () => {\n if (!serverTimer) return;\n await stopTimerMutation({ variables: { id: serverTimer.timeEntry.id } });\n await refetchRunning();\n };\n\n const handleReset = async () => {\n if (!resetTarget) return;\n // Discard the in-progress session: stop it, then delete the entry.\n await stopTimerMutation({ variables: { id: resetTarget.timeEntry.id } });\n await deleteTimeEntry({ variables: { id: resetTarget.timeEntry.id } });\n setResetTarget(null);\n await refetchRunning();\n };\n\n const isRunning = Boolean(serverTimer);\n\n return (\n <PageLayout\n title=\"Timer\"\n description=\"Focus timer for deep work sessions\"\n purpose=\"A single, distraction-free stopwatch for the task in front of you — start it when you begin focused work and it logs the session as a time entry you can attribute to a project later.\"\n >\n <div className=\"mx-auto max-w-lg\">\n <NextSteps\n storageKey=\"timecampus-timer\"\n className=\"mb-6\"\n steps={[\n {\n id: \"start\",\n label: isRunning ? \"Stop the running timer\" : \"Start the timer\",\n description: isRunning\n ? \"End this session and save it as a time entry.\"\n : \"Begin tracking the task you're working on right now.\",\n onClick: () => (isRunning ? handleStop() : setStartOpen(true)),\n },\n {\n id: \"assign-project\",\n label: \"Assign work to a project\",\n description: \"Pick a project so this time bills correctly.\",\n onClick: () => navigate(\"/projects\"),\n },\n {\n id: \"review-entries\",\n label: \"Review your time entries\",\n description: \"Check and tidy everything you've logged.\",\n onClick: () => navigate(\"/time/entries\"),\n },\n ]}\n />\n <GlassCard treatment=\"glass\" glow className=\"p-6 text-center sm:p-8\">\n <p className=\"font-mono text-5xl font-bold tabular-nums text-text-primary sm:text-6xl\">\n {activeTimer\n ? formatTimerDuration(activeTimer.seconds)\n : \"00:00:00\"}\n </p>\n {activeTimer?.description && (\n <p className=\"mt-2 text-sm text-text-secondary\">\n {activeTimer.description}\n </p>\n )}\n <div className=\"mt-6 flex flex-wrap items-center justify-center gap-3\">\n {isRunning ? (\n <button\n onClick={handleStop}\n disabled={stopping}\n className=\"inline-flex items-center gap-2 rounded-md bg-action-danger-bg px-6 py-3 text-sm font-medium text-action-primary-text hover:bg-action-danger-bg/90 disabled:opacity-50\"\n >\n <Square className=\"h-4 w-4\" />\n {stopping ? \"Stopping...\" : \"Stop\"}\n </button>\n ) : (\n <button\n onClick={() => setStartOpen(true)}\n disabled={starting}\n className=\"inline-flex items-center gap-2 rounded-md bg-action-primary-bg px-6 py-3 text-sm font-medium text-action-primary-text hover:bg-action-primary-bg/90 disabled:opacity-50\"\n >\n <Play className=\"h-4 w-4\" />\n Start\n </button>\n )}\n <button\n onClick={() => serverTimer && setResetTarget(serverTimer)}\n disabled={!isRunning || deleting}\n className=\"inline-flex items-center gap-2 rounded-md border border-border-subtle px-4 py-3 text-sm font-medium text-text-primary hover:bg-bg-sunken/50 disabled:opacity-50\"\n title=\"Discard the running timer without saving\"\n >\n <RotateCcw className=\"h-4 w-4\" />\n Reset\n </button>\n </div>\n </GlassCard>\n\n {/* Server-side running timers */}\n <GlassCard treatment=\"glass\" className=\"mt-6 p-6\">\n <div className=\"mb-4 flex items-center justify-between\">\n <h3 className=\"text-sm font-semibold text-text-primary\">\n Running Timers\n </h3>\n {runningTimers.length > 0 && (\n <StatusBadge\n label={`${runningTimers.length} active`}\n variant=\"success\"\n dot\n />\n )}\n </div>\n {runningLoading && runningTimers.length === 0 ? (\n <LoadingSkeleton variant=\"list\" count={2} />\n ) : runningTimers.length === 0 ? (\n <EmptyState\n illustration=\"empty-sessions\"\n title=\"No active timers on the server\"\n description=\"Timers you start sync here so you can pick them up on any device.\"\n />\n ) : (\n <div className=\"flex flex-col gap-2\">\n {runningTimers.map((timer: RunningTimerItem) => (\n <div\n key={timer.timeEntry.id}\n className=\"flex items-center gap-3 rounded-lg border border-border-subtle bg-bg-surface p-3\"\n >\n <span className=\"flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-action-primary-bg/10 text-xs font-semibold text-primary\">\n {getInitials(timer.timeEntry.project?.name ?? \"No Project\")}\n </span>\n <div className=\"min-w-0 flex-1\">\n <p className=\"truncate text-sm font-medium text-text-primary\">\n {timer.timeEntry.project?.name ?? \"No Project\"}\n </p>\n <p className=\"truncate text-xs text-text-secondary\">\n {timer.timeEntry.description ?? \"No description\"}\n </p>\n </div>\n <p className=\"ml-2 shrink-0 font-mono text-sm font-medium text-text-primary\">\n {formatTimerDuration(timer.elapsedSeconds)}\n </p>\n </div>\n ))}\n </div>\n )}\n </GlassCard>\n </div>\n\n {/* Start timer modal */}\n <Modal\n open={startOpen}\n onClose={() => setStartOpen(false)}\n title=\"Start a timer\"\n description=\"Track the task you're working on right now.\"\n footer={\n <>\n <button\n onClick={() => setStartOpen(false)}\n className=\"rounded-md border border-border-subtle px-4 py-2 text-sm font-medium text-text-primary hover:bg-bg-sunken/50\"\n >\n Cancel\n </button>\n <button\n onClick={handleStart}\n disabled={starting}\n className=\"rounded-md bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text hover:bg-action-primary-bg/90 disabled:opacity-50\"\n >\n {starting ? \"Starting...\" : \"Start Timer\"}\n </button>\n </>\n }\n >\n {startError && (\n <div className=\"mb-4 rounded-lg border border-status-error-bg/40 bg-status-error-bg p-3 text-sm text-status-error-text\">\n Failed to start the timer. Please try again.\n </div>\n )}\n <div className=\"space-y-4\">\n <div>\n <label className=\"block text-sm font-medium text-text-primary\">\n What are you working on?\n </label>\n <input\n type=\"text\"\n placeholder=\"Description\"\n value={description}\n onChange={(e) => setDescription(e.target.value)}\n className=\"mt-1 w-full rounded-md border border-border-subtle bg-bg-surface px-3 py-2 text-sm text-text-primary placeholder:text-text-secondary\"\n />\n </div>\n <div>\n <label className=\"block text-sm font-medium text-text-primary\">\n Project\n </label>\n <select\n value={projectId}\n onChange={(e) => setProjectId(e.target.value)}\n className=\"mt-1 w-full rounded-md border border-border-subtle bg-bg-surface px-3 py-2 text-sm text-text-primary\"\n >\n <option value=\"\">No Project</option>\n {projects.map((project) => (\n <option key={project.id} value={project.id}>\n {project.name}\n </option>\n ))}\n </select>\n </div>\n <label className=\"flex items-center gap-2 text-sm text-text-primary\">\n <input\n type=\"checkbox\"\n checked={billable}\n onChange={(e) => setBillable(e.target.checked)}\n className=\"h-4 w-4 rounded border-border-subtle\"\n />\n Billable\n </label>\n </div>\n </Modal>\n\n {/* Reset / discard confirm modal */}\n <Modal\n open={Boolean(resetTarget)}\n onClose={() => setResetTarget(null)}\n title=\"Discard running timer?\"\n description=\"This stops the timer and deletes the in-progress entry. It cannot be undone.\"\n footer={\n <>\n <button\n onClick={() => setResetTarget(null)}\n className=\"rounded-md border border-border-subtle px-4 py-2 text-sm font-medium text-text-primary hover:bg-bg-sunken/50\"\n >\n Cancel\n </button>\n <button\n onClick={handleReset}\n disabled={stopping || deleting}\n className=\"rounded-md bg-action-danger-bg px-4 py-2 text-sm font-medium text-action-primary-text hover:bg-action-danger-bg/90 disabled:opacity-50\"\n >\n {stopping || deleting ? \"Discarding...\" : \"Discard\"}\n </button>\n </>\n }\n >\n <p className=\"text-sm text-text-secondary\">\n The timer for{\" \"}\n <span className=\"font-medium text-text-primary\">\n {resetTarget?.timeEntry.project?.name ??\n resetTarget?.timeEntry.description ??\n \"this session\"}\n </span>{\" \"}\n will be discarded.\n </p>\n </Modal>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"TimerPage.js","names":[],"sources":["../../../../src/timecampus/pages/time/TimerPage.tsx"],"sourcesContent":["import { useEffect, useState } from \"react\";\nimport { Play, Square, RotateCcw } from \"lucide-react\";\nimport { GlassCard, NextSteps } from \"@burdenoff/fe-libs/ui\";\nimport { PageLayout } from \"../../components/PageLayout\";\nimport { Modal } from \"../../components/Modal\";\nimport { EmptyState } from \"../../components/EmptyState\";\nimport { LoadingSkeleton } from \"../../components/LoadingSkeleton\";\nimport { StatusBadge } from \"../../components/StatusBadge\";\nimport { useTimeCampusNavigate } from \"../../providers/TimeCampusProvider\";\nimport { formatTimerDuration, getInitials } from \"../../utils/helpers\";\nimport { useTimeCampusStore } from \"../../store/timecampusStore\";\nimport { useTr } from \"../../hooks\";\nimport {\n useListRunningTimersQuery,\n useStartTimerMutation,\n useStopTimerMutation,\n useDeleteTimeEntryMutation,\n useListProjectsQuery,\n ListRunningTimersDocument,\n ListTimeEntriesDocument,\n GetTimeEntryStatsDocument,\n type ListRunningTimersQuery,\n type ListProjectsQuery,\n} from \"@/generated/wspace-operations\";\n\ntype RunningTimerItem = ListRunningTimersQuery[\"runningTimers\"][number];\ntype ProjectOption = ListProjectsQuery[\"projects\"][\"items\"][number];\n\nexport function TimerPage() {\n const { activeTimer, tickTimer, syncTimerFromServer } = useTimeCampusStore();\n const navigate = useTimeCampusNavigate();\n const t = useTr();\n\n const [startOpen, setStartOpen] = useState(false);\n const [description, setDescription] = useState(\"\");\n const [projectId, setProjectId] = useState(\"\");\n const [billable, setBillable] = useState(false);\n const [resetTarget, setResetTarget] = useState<RunningTimerItem | null>(null);\n\n const {\n data: runningData,\n loading: runningLoading,\n refetch: refetchRunning,\n } = useListRunningTimersQuery({ fetchPolicy: \"cache-and-network\" });\n\n const { data: projectsData } = useListProjectsQuery({\n variables: { pagination: { limit: 50 } },\n });\n const projects: ProjectOption[] = projectsData?.projects?.items ?? [];\n\n const runningTimers = runningData?.runningTimers ?? [];\n const serverTimer = runningTimers[0] ?? null;\n\n const refetchAll = [\n { query: ListRunningTimersDocument },\n {\n query: ListTimeEntriesDocument,\n variables: { pagination: { limit: 50 } },\n },\n { query: GetTimeEntryStatsDocument },\n ];\n\n const [startTimerMutation, { loading: starting, error: startError }] =\n useStartTimerMutation({ refetchQueries: refetchAll });\n const [stopTimerMutation, { loading: stopping }] = useStopTimerMutation({\n refetchQueries: refetchAll,\n });\n const [deleteTimeEntry, { loading: deleting }] = useDeleteTimeEntryMutation({\n refetchQueries: refetchAll,\n });\n\n // Backend is the source of truth for whether a timer is running — mirror the\n // server running timer into the local display store whenever it changes.\n useEffect(() => {\n if (serverTimer) {\n syncTimerFromServer({\n timeEntryId: serverTimer.timeEntry.id,\n elapsedSeconds: serverTimer.elapsedSeconds,\n projectId: serverTimer.timeEntry.projectId ?? undefined,\n taskId: serverTimer.timeEntry.taskId ?? undefined,\n description: serverTimer.timeEntry.description ?? undefined,\n });\n } else if (!runningLoading) {\n syncTimerFromServer(null);\n }\n // Only re-sync when the server's running timer identity/elapsed changes.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [serverTimer?.timeEntry.id, serverTimer?.elapsedSeconds, runningLoading]);\n\n // Local ticking display for the live seconds counter.\n useEffect(() => {\n if (!activeTimer?.isRunning) return;\n const interval = setInterval(() => tickTimer(), 1000);\n return () => clearInterval(interval);\n }, [activeTimer?.isRunning, tickTimer]);\n\n const handleStart = async () => {\n await startTimerMutation({\n variables: {\n input: {\n description: description.trim() || undefined,\n projectId: projectId || undefined,\n billable,\n source: \"timer\",\n },\n },\n });\n setStartOpen(false);\n setDescription(\"\");\n setProjectId(\"\");\n setBillable(false);\n await refetchRunning();\n };\n\n const handleStop = async () => {\n if (!serverTimer) return;\n await stopTimerMutation({ variables: { id: serverTimer.timeEntry.id } });\n await refetchRunning();\n };\n\n const handleReset = async () => {\n if (!resetTarget) return;\n // Discard the in-progress session: stop it, then delete the entry.\n await stopTimerMutation({ variables: { id: resetTarget.timeEntry.id } });\n await deleteTimeEntry({ variables: { id: resetTarget.timeEntry.id } });\n setResetTarget(null);\n await refetchRunning();\n };\n\n const isRunning = Boolean(serverTimer);\n\n return (\n <PageLayout\n title={t(\"timecampus.timer.title\", \"Timer\")}\n description={t(\n \"timecampus.timer.description\",\n \"Focus timer for deep work sessions\",\n )}\n purpose={t(\n \"timecampus.timer.purpose\",\n \"A single, distraction-free stopwatch for the task in front of you — start it when you begin focused work and it logs the session as a time entry you can attribute to a project later.\",\n )}\n >\n <div className=\"mx-auto max-w-lg\">\n <NextSteps\n storageKey=\"timecampus-timer\"\n className=\"mb-6\"\n steps={[\n {\n id: \"start\",\n label: isRunning\n ? t(\n \"timecampus.timer.nextSteps.stopRunning\",\n \"Stop the running timer\",\n )\n : t(\"timecampus.timer.nextSteps.start\", \"Start the timer\"),\n description: isRunning\n ? t(\n \"timecampus.timer.nextSteps.stopDescription\",\n \"End this session and save it as a time entry.\",\n )\n : t(\n \"timecampus.timer.nextSteps.startDescription\",\n \"Begin tracking the task you're working on right now.\",\n ),\n onClick: () => (isRunning ? handleStop() : setStartOpen(true)),\n },\n {\n id: \"assign-project\",\n label: t(\n \"timecampus.timer.nextSteps.assignProject\",\n \"Assign work to a project\",\n ),\n description: t(\n \"timecampus.timer.nextSteps.assignDescription\",\n \"Pick a project so this time bills correctly.\",\n ),\n onClick: () => navigate(\"/projects\"),\n },\n {\n id: \"review-entries\",\n label: t(\n \"timecampus.timer.nextSteps.review\",\n \"Review your time entries\",\n ),\n description: t(\n \"timecampus.timer.nextSteps.reviewDescription\",\n \"Check and tidy everything you've logged.\",\n ),\n onClick: () => navigate(\"/time/entries\"),\n },\n ]}\n />\n <GlassCard treatment=\"glass\" glow className=\"p-6 text-center sm:p-8\">\n <p className=\"font-mono text-5xl font-bold tabular-nums text-text-primary sm:text-6xl\">\n {activeTimer\n ? formatTimerDuration(activeTimer.seconds)\n : t(\"timecampus.timer.elapsedDefault\", \"00:00:00\")}\n </p>\n {activeTimer?.description && (\n <p className=\"mt-2 text-sm text-text-secondary\">\n {activeTimer.description}\n </p>\n )}\n <div className=\"mt-6 flex flex-wrap items-center justify-center gap-3\">\n {isRunning ? (\n <button\n onClick={handleStop}\n disabled={stopping}\n className=\"inline-flex items-center gap-2 rounded-md bg-action-danger-bg px-6 py-3 text-sm font-medium text-action-primary-text hover:bg-action-danger-bg/90 disabled:opacity-50\"\n >\n <Square className=\"h-4 w-4\" />\n {stopping\n ? t(\"timecampus.timer.stopping\", \"Stopping...\")\n : t(\"timecampus.timer.stop\", \"Stop\")}\n </button>\n ) : (\n <button\n onClick={() => setStartOpen(true)}\n disabled={starting}\n className=\"inline-flex items-center gap-2 rounded-md bg-action-primary-bg px-6 py-3 text-sm font-medium text-action-primary-text hover:bg-action-primary-bg/90 disabled:opacity-50\"\n >\n <Play className=\"h-4 w-4\" />\n {t(\"timecampus.timer.start\", \"Start\")}\n </button>\n )}\n <button\n onClick={() => serverTimer && setResetTarget(serverTimer)}\n disabled={!isRunning || deleting}\n className=\"inline-flex items-center gap-2 rounded-md border border-border-subtle px-4 py-3 text-sm font-medium text-text-primary hover:bg-bg-sunken/50 disabled:opacity-50\"\n title={t(\n \"timecampus.timer.resetTooltip\",\n \"Discard the running timer without saving\",\n )}\n >\n <RotateCcw className=\"h-4 w-4\" />\n {t(\"timecampus.timer.reset\", \"Reset\")}\n </button>\n </div>\n </GlassCard>\n\n {/* Server-side running timers */}\n <GlassCard treatment=\"glass\" className=\"mt-6 p-6\">\n <div className=\"mb-4 flex items-center justify-between\">\n <h3 className=\"text-sm font-semibold text-text-primary\">\n {t(\"timecampus.timer.runningTimers.title\", \"Running Timers\")}\n </h3>\n {runningTimers.length > 0 && (\n <StatusBadge\n label={t(\n \"timecampus.timer.runningTimers.active\",\n \"{{count}} active\",\n { count: runningTimers.length },\n )}\n variant=\"success\"\n dot\n />\n )}\n </div>\n {runningLoading && runningTimers.length === 0 ? (\n <LoadingSkeleton variant=\"list\" count={2} />\n ) : runningTimers.length === 0 ? (\n <EmptyState\n illustration=\"empty-sessions\"\n title={t(\n \"timecampus.timer.runningTimers.emptyTitle\",\n \"No active timers on the server\",\n )}\n description={t(\n \"timecampus.timer.runningTimers.emptyDescription\",\n \"Timers you start sync here so you can pick them up on any device.\",\n )}\n />\n ) : (\n <div className=\"flex flex-col gap-2\">\n {runningTimers.map((timer: RunningTimerItem) => (\n <div\n key={timer.timeEntry.id}\n className=\"flex items-center gap-3 rounded-lg border border-border-subtle bg-bg-surface p-3\"\n >\n <span className=\"flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-action-primary-bg/10 text-xs font-semibold text-primary\">\n {getInitials(\n timer.timeEntry.project?.name ??\n t(\n \"timecampus.timer.runningTimers.noProject\",\n \"No Project\",\n ),\n )}\n </span>\n <div className=\"min-w-0 flex-1\">\n <p className=\"truncate text-sm font-medium text-text-primary\">\n {timer.timeEntry.project?.name ??\n t(\n \"timecampus.timer.runningTimers.noProject\",\n \"No Project\",\n )}\n </p>\n <p className=\"truncate text-xs text-text-secondary\">\n {timer.timeEntry.description ??\n t(\n \"timecampus.timer.runningTimers.noDescription\",\n \"No description\",\n )}\n </p>\n </div>\n <p className=\"ml-2 shrink-0 font-mono text-sm font-medium text-text-primary\">\n {formatTimerDuration(timer.elapsedSeconds)}\n </p>\n </div>\n ))}\n </div>\n )}\n </GlassCard>\n </div>\n\n {/* Start timer modal */}\n <Modal\n open={startOpen}\n onClose={() => setStartOpen(false)}\n title={t(\"timecampus.timer.modal.startTitle\", \"Start a timer\")}\n description={t(\n \"timecampus.timer.modal.startDescription\",\n \"Track the task you're working on right now.\",\n )}\n footer={\n <>\n <button\n onClick={() => setStartOpen(false)}\n className=\"rounded-md border border-border-subtle px-4 py-2 text-sm font-medium text-text-primary hover:bg-bg-sunken/50\"\n >\n {t(\"timecampus.timer.modal.cancel\", \"Cancel\")}\n </button>\n <button\n onClick={handleStart}\n disabled={starting}\n className=\"rounded-md bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text hover:bg-action-primary-bg/90 disabled:opacity-50\"\n >\n {starting\n ? t(\"timecampus.timer.modal.starting\", \"Starting...\")\n : t(\"timecampus.timer.modal.startTimer\", \"Start Timer\")}\n </button>\n </>\n }\n >\n {startError && (\n <div className=\"mb-4 rounded-lg border border-status-error-bg/40 bg-status-error-bg p-3 text-sm text-status-error-text\">\n {t(\n \"timecampus.timer.modal.startError\",\n \"Failed to start the timer. Please try again.\",\n )}\n </div>\n )}\n <div className=\"space-y-4\">\n <div>\n <label className=\"block text-sm font-medium text-text-primary\">\n {t(\n \"timecampus.timer.modal.descriptionLabel\",\n \"What are you working on?\",\n )}\n </label>\n <input\n type=\"text\"\n placeholder={t(\n \"timecampus.timer.modal.descriptionPlaceholder\",\n \"Description\",\n )}\n value={description}\n onChange={(e) => setDescription(e.target.value)}\n className=\"mt-1 w-full rounded-md border border-border-subtle bg-bg-surface px-3 py-2 text-sm text-text-primary placeholder:text-text-secondary\"\n />\n </div>\n <div>\n <label className=\"block text-sm font-medium text-text-primary\">\n {t(\"timecampus.timer.modal.projectLabel\", \"Project\")}\n </label>\n <select\n value={projectId}\n onChange={(e) => setProjectId(e.target.value)}\n className=\"mt-1 w-full rounded-md border border-border-subtle bg-bg-surface px-3 py-2 text-sm text-text-primary\"\n >\n <option value=\"\">\n {t(\"timecampus.timer.modal.noProject\", \"No Project\")}\n </option>\n {projects.map((project) => (\n <option key={project.id} value={project.id}>\n {project.name}\n </option>\n ))}\n </select>\n </div>\n <label className=\"flex items-center gap-2 text-sm text-text-primary\">\n <input\n type=\"checkbox\"\n checked={billable}\n onChange={(e) => setBillable(e.target.checked)}\n className=\"h-4 w-4 rounded border-border-subtle\"\n />\n {t(\"timecampus.timer.modal.billable\", \"Billable\")}\n </label>\n </div>\n </Modal>\n\n {/* Reset / discard confirm modal */}\n <Modal\n open={Boolean(resetTarget)}\n onClose={() => setResetTarget(null)}\n title={t(\n \"timecampus.timer.modal.discardTitle\",\n \"Discard running timer?\",\n )}\n description={t(\n \"timecampus.timer.modal.discardDescription\",\n \"This stops the timer and deletes the in-progress entry. It cannot be undone.\",\n )}\n footer={\n <>\n <button\n onClick={() => setResetTarget(null)}\n className=\"rounded-md border border-border-subtle px-4 py-2 text-sm font-medium text-text-primary hover:bg-bg-sunken/50\"\n >\n {t(\"timecampus.timer.modal.cancel\", \"Cancel\")}\n </button>\n <button\n onClick={handleReset}\n disabled={stopping || deleting}\n className=\"rounded-md bg-action-danger-bg px-4 py-2 text-sm font-medium text-action-primary-text hover:bg-action-danger-bg/90 disabled:opacity-50\"\n >\n {stopping || deleting\n ? t(\"timecampus.timer.modal.discarding\", \"Discarding...\")\n : t(\"timecampus.timer.modal.discard\", \"Discard\")}\n </button>\n </>\n }\n >\n <p className=\"text-sm text-text-secondary\">\n {t(\"timecampus.timer.modal.discardMessagePrefix\", \"The timer for\")}{\" \"}\n <span className=\"font-medium text-text-primary\">\n {resetTarget?.timeEntry.project?.name ??\n resetTarget?.timeEntry.description ??\n t(\"timecampus.timer.modal.sessionFallback\", \"this session\")}\n </span>{\" \"}\n {t(\n \"timecampus.timer.modal.discardMessageSuffix\",\n \"will be discarded.\",\n )}\n </p>\n </Modal>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"TimelinePage.d.ts","sourceRoot":"","sources":["../../../../src/timecampus/pages/timeline/TimelinePage.tsx"],"names":[],"mappings":"AAiQA,wBAAgB,YAAY,gCAmP3B"}
1
+ {"version":3,"file":"TimelinePage.d.ts","sourceRoot":"","sources":["../../../../src/timecampus/pages/timeline/TimelinePage.tsx"],"names":[],"mappings":"AAiQA,wBAAgB,YAAY,gCAiP3B"}
@@ -292,13 +292,13 @@ function H() {
292
292
  }),
293
293
  /* @__PURE__ */ S("div", {
294
294
  className: "flex flex-wrap gap-4",
295
- children: F.filter((e) => Z.has(e)).map((e) => {
295
+ children: F.map((e) => {
296
296
  let t = N[e], n = Z.get(e);
297
297
  return /* @__PURE__ */ S(R, {
298
298
  category: e,
299
299
  label: t.label,
300
300
  dotClassName: t.dot,
301
- totalSeconds: n.totalSeconds
301
+ totalSeconds: n?.totalSeconds ?? 0
302
302
  }, e);
303
303
  })
304
304
  })
@@ -1 +1 @@
1
- {"version":3,"file":"TimelinePage.js","names":[],"sources":["../../../../src/timecampus/pages/timeline/TimelinePage.tsx"],"sourcesContent":["import { useMemo, useState } from \"react\";\nimport {\n DndContext,\n DragOverlay,\n KeyboardSensor,\n PointerSensor,\n useDraggable,\n useDroppable,\n useSensor,\n useSensors,\n type DragEndEvent,\n type DragStartEvent,\n} from \"@dnd-kit/core\";\nimport {\n Activity,\n Brain,\n Calendar,\n Coffee,\n FolderOpen,\n Github,\n GripVertical,\n MessageSquare,\n Users,\n Zap,\n} from \"lucide-react\";\nimport { GlassCard } from \"@burdenoff/fe-libs/ui\";\nimport { PageLayout } from \"../../components/PageLayout\";\nimport { StatCard } from \"../../components/StatCard\";\nimport { EmptyState } from \"../../components/EmptyState\";\nimport { LoadingSkeleton } from \"../../components/LoadingSkeleton\";\nimport { formatDuration, formatTime } from \"../../utils/helpers\";\nimport { useTimeCampus } from \"../../providers/TimeCampusProvider\";\nimport {\n useGetTimeCampusTimelineQuery,\n useListProjectsQuery,\n useRecategorizeTimeCampusCaptureEventMutation,\n type TimelineSegmentFieldsFragment,\n} from \"@/generated/wspace-operations\";\n\ntype Category = \"MEETING\" | \"DEEP_WORK\" | \"COMMUNICATION\" | \"BREAK\" | \"OTHER\";\n\nconst CATEGORY_META: Record<\n Category,\n { label: string; icon: typeof Brain; dot: string; chip: string }\n> = {\n DEEP_WORK: {\n label: \"Deep work\",\n icon: Brain,\n dot: \"bg-status-success-bg\",\n chip: \"bg-status-success-bg text-status-success-text\",\n },\n MEETING: {\n label: \"Meeting\",\n icon: Users,\n dot: \"bg-status-info-bg\",\n chip: \"bg-status-info-bg text-status-info-text\",\n },\n COMMUNICATION: {\n label: \"Communication\",\n icon: MessageSquare,\n dot: \"bg-status-warning-bg\",\n chip: \"bg-status-warning-bg text-status-warning-text\",\n },\n BREAK: {\n label: \"Break\",\n icon: Coffee,\n dot: \"bg-bg-sunken\",\n chip: \"bg-bg-sunken text-text-secondary\",\n },\n OTHER: {\n label: \"Other\",\n icon: Activity,\n dot: \"bg-bg-sunken\",\n chip: \"bg-bg-sunken text-text-secondary\",\n },\n};\n\nconst SOURCE_ICON: Record<string, typeof Brain> = {\n TIME_ENTRY: Zap,\n MEETING: Users,\n POMODORO: Brain,\n EMAIL: MessageSquare,\n CAPTURE: Github,\n};\n\nconst CATEGORY_ORDER: Category[] = [\n \"DEEP_WORK\",\n \"MEETING\",\n \"COMMUNICATION\",\n \"BREAK\",\n \"OTHER\",\n];\n\n/**\n * How a segment came by its category. The service writes these lowercase\n * (`timeline.service.ts` → 'auto' | 'user' | 'channel'), so normalise before\n * matching rather than trusting the string's casing.\n */\nconst CATEGORY_SOURCE_LABEL: Record<string, string> = {\n auto: \"Auto-categorized\",\n user: \"You set this\",\n channel: \"From channel rules\",\n};\n\nfunction categorySourceLabel(source: string | null | undefined): string | null {\n if (!source) return null;\n return CATEGORY_SOURCE_LABEL[source.toLowerCase()] ?? source;\n}\n\n/** Droppable target: a category chip in the \"Where the time went\" summary. */\nfunction DroppableCategoryChip({\n category,\n label,\n dotClassName,\n totalSeconds,\n}: {\n category: Category;\n label: string;\n dotClassName: string;\n totalSeconds: number;\n}) {\n const { isOver, setNodeRef } = useDroppable({ id: `category:${category}` });\n return (\n <div\n ref={setNodeRef}\n className={`flex items-center gap-2 rounded-lg border px-3 py-2 transition-colors ${\n isOver\n ? \"border-action-primary-bg bg-action-primary-bg/10\"\n : \"border-border-seam bg-bg-surface\"\n }`}\n >\n <span\n className={`inline-block h-2.5 w-2.5 rounded-full ${dotClassName}`}\n aria-hidden\n />\n <span className=\"text-sm font-medium text-text-primary\">{label}</span>\n <span className=\"text-sm tabular-nums text-text-secondary\">\n {formatDuration(totalSeconds)}\n </span>\n {isOver && (\n <span className=\"text-xs font-medium text-primary\">Drop here</span>\n )}\n </div>\n );\n}\n\n/** Draggable row for a single timeline segment, with the existing dropdown kept as an accessible fallback. */\nfunction DraggableSegmentRow({\n segment,\n category,\n projectName,\n recategorizing,\n onRecategorize,\n}: {\n segment: TimelineSegmentFieldsFragment;\n category: Category;\n /** Resolved name for `segment.projectId`; undefined when it carries none. */\n projectName: string | undefined;\n recategorizing: boolean;\n onRecategorize: (\n segment: TimelineSegmentFieldsFragment,\n category: Category,\n ) => void;\n}) {\n const meta = CATEGORY_META[category] ?? CATEGORY_META.OTHER;\n const SourceIcon = SOURCE_ICON[segment.source] ?? Activity;\n const provenance = categorySourceLabel(segment.categorySource);\n const { attributes, listeners, setNodeRef, isDragging } = useDraggable({\n id: segment.id,\n data: { segment },\n disabled: !segment.isRecategorizable,\n });\n\n return (\n <li\n ref={setNodeRef}\n className={`flex items-start gap-3 p-4 transition-colors hover:bg-bg-sunken/50 ${\n isDragging ? \"opacity-40\" : \"\"\n }`}\n >\n <div className=\"flex w-16 shrink-0 flex-col items-end pt-0.5 text-xs tabular-nums text-text-secondary\">\n <span>{formatTime(segment.startTime)}</span>\n <span className=\"text-text-tertiary\">\n {formatDuration(segment.durationSeconds)}\n </span>\n </div>\n {segment.isRecategorizable && (\n <button\n type=\"button\"\n aria-label={`Drag to recategorize ${segment.title || meta.label}`}\n className=\"mt-1.5 shrink-0 cursor-grab touch-none text-text-tertiary hover:text-text-secondary active:cursor-grabbing\"\n {...attributes}\n {...listeners}\n >\n <GripVertical className=\"h-4 w-4\" />\n </button>\n )}\n <div\n className={`mt-0.5 flex h-8 w-8 shrink-0 items-center justify-center rounded-lg ${meta.chip}`}\n >\n <SourceIcon className=\"h-4 w-4\" />\n </div>\n <div className=\"min-w-0 flex-1\">\n <p className=\"truncate text-sm font-medium text-text-primary\">\n {segment.title || meta.label}\n </p>\n <div className=\"mt-1 flex flex-wrap items-center gap-2\">\n <span\n className={`inline-flex rounded-full px-2 py-0.5 text-xs font-medium ${meta.chip}`}\n >\n {meta.label}\n </span>\n {projectName !== undefined && (\n <span className=\"inline-flex max-w-[12rem] items-center gap-1 rounded-full bg-bg-sunken px-2 py-0.5 text-xs font-medium text-text-secondary\">\n <FolderOpen className=\"h-3 w-3 shrink-0\" />\n <span className=\"truncate\">{projectName}</span>\n </span>\n )}\n {segment.app && (\n <span className=\"text-xs text-text-secondary\">{segment.app}</span>\n )}\n {provenance && (\n <span className=\"text-xs text-text-tertiary\">{provenance}</span>\n )}\n </div>\n </div>\n {segment.isRecategorizable && (\n <select\n aria-label=\"Recategorize activity\"\n disabled={recategorizing}\n value={category}\n onChange={(e) => onRecategorize(segment, e.target.value as Category)}\n className=\"shrink-0 rounded-md border border-border-seam bg-bg-surface px-2 py-1 text-xs text-text-secondary focus:outline-none focus:ring-2 focus:ring-action-primary-bg/40\"\n >\n {CATEGORY_ORDER.map((c) => (\n <option key={c} value={c}>\n {CATEGORY_META[c].label}\n </option>\n ))}\n </select>\n )}\n </li>\n );\n}\n\nfunction toLocalDateInput(date: Date): string {\n const offset = date.getTimezoneOffset();\n const local = new Date(date.getTime() - offset * 60 * 1000);\n return local.toISOString().slice(0, 10);\n}\n\nfunction dayBounds(dateStr: string): { from: string; to: string } {\n const start = new Date(`${dateStr}T00:00:00`);\n const end = new Date(`${dateStr}T23:59:59.999`);\n return { from: start.toISOString(), to: end.toISOString() };\n}\n\nexport function TimelinePage() {\n const { workspaceId = \"\" } = useTimeCampus();\n const [dateStr, setDateStr] = useState(() => toLocalDateInput(new Date()));\n const [draggingSegment, setDraggingSegment] =\n useState<TimelineSegmentFieldsFragment | null>(null);\n\n const { from, to } = useMemo(() => dayBounds(dateStr), [dateStr]);\n\n const { data, loading, error, refetch } = useGetTimeCampusTimelineQuery({\n variables: { from, to },\n skip: !workspaceId,\n fetchPolicy: \"cache-and-network\",\n });\n\n // Segments carry a `projectId` only; the workspace's projects supply the\n // name to show next to them. A segment whose id isn't in this list simply\n // keeps no project chip — it is never labelled from a guess.\n const { data: projectsData } = useListProjectsQuery({\n variables: { pagination: { limit: 200 } },\n skip: !workspaceId,\n });\n\n const [recategorize, { loading: recategorizing }] =\n useRecategorizeTimeCampusCaptureEventMutation();\n\n const sensors = useSensors(\n useSensor(PointerSensor, { activationConstraint: { distance: 6 } }),\n useSensor(KeyboardSensor),\n );\n\n const timeline = data?.timeCampusTimeline;\n const segments = timeline?.segments ?? [];\n const summary = timeline?.summary;\n\n const projectNameById = useMemo(() => {\n const map = new Map<string, string>();\n for (const project of projectsData?.projects?.items ?? []) {\n map.set(project.id, project.name);\n }\n return map;\n }, [projectsData?.projects?.items]);\n\n const totalsByCategory = useMemo(() => {\n const map = new Map<\n string,\n { totalSeconds: number; segmentCount: number }\n >();\n for (const t of summary?.byCategory ?? []) {\n map.set(t.category, {\n totalSeconds: t.totalSeconds,\n segmentCount: t.segmentCount,\n });\n }\n return map;\n }, [summary]);\n\n const handleRecategorize = async (\n segment: TimelineSegmentFieldsFragment,\n category: Category,\n ) => {\n if (!segment.isRecategorizable || segment.category === category) return;\n await recategorize({\n variables: { id: segment.sourceId, category },\n });\n await refetch();\n };\n\n const handleDragStart = (event: DragStartEvent) => {\n const segment = event.active.data.current?.segment as\n TimelineSegmentFieldsFragment | undefined;\n setDraggingSegment(segment ?? null);\n };\n\n const handleDragEnd = (event: DragEndEvent) => {\n const segment = event.active.data.current?.segment as\n TimelineSegmentFieldsFragment | undefined;\n setDraggingSegment(null);\n const overId = event.over?.id;\n if (!segment || typeof overId !== \"string\") return;\n const category = overId.replace(/^category:/, \"\") as Category;\n if (!CATEGORY_ORDER.includes(category)) return;\n void handleRecategorize(segment, category);\n };\n\n const focusPct =\n summary && summary.focusRatio != null\n ? Math.round(summary.focusRatio * 100)\n : 0;\n\n // The real span of the day, straight off the summary — not inferred from the\n // segment list, which may be paginated or filtered differently.\n const daySpan =\n summary?.firstActivityAt && summary?.lastActivityAt\n ? `${formatTime(summary.firstActivityAt)} – ${formatTime(summary.lastActivityAt)}`\n : null;\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 <div className=\"flex flex-wrap items-center gap-3\">\n {daySpan && (\n <span className=\"inline-flex items-center gap-1.5 rounded-full bg-bg-sunken px-2.5 py-1 text-xs font-medium tabular-nums text-text-secondary\">\n <Activity className=\"h-3.5 w-3.5\" />\n {daySpan}\n </span>\n )}\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 </div>\n }\n >\n {loading && !timeline ? (\n <div className=\"space-y-6\">\n <LoadingSkeleton variant=\"card\" count={4} />\n <LoadingSkeleton variant=\"list\" count={6} />\n </div>\n ) : error ? (\n <div className=\"rounded-lg border border-status-error-bg/40 bg-status-error-bg p-4 text-sm text-status-error-text\">\n Failed to load your timeline. Please try again later.\n </div>\n ) : (\n <DndContext\n sensors={sensors}\n onDragStart={handleDragStart}\n onDragEnd={handleDragEnd}\n >\n <div className=\"space-y-6\">\n {/* Summary */}\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4\">\n <StatCard\n featured\n title=\"Tracked time\"\n value={formatDuration(summary?.totalTrackedSeconds ?? 0)}\n icon={<Activity className=\"h-5 w-5\" />}\n description=\"Total activity across the day\"\n />\n <StatCard\n title=\"Focus ratio\"\n value={`${focusPct}%`}\n icon={<Brain className=\"h-5 w-5\" />}\n description=\"Share of time in deep work\"\n />\n <StatCard\n title=\"Context switches\"\n value={summary?.contextSwitches ?? 0}\n icon={<Zap className=\"h-5 w-5\" />}\n description=\"Category changes through the day\"\n />\n <StatCard\n title=\"Longest focus block\"\n value={formatDuration(summary?.longestFocusBlockSeconds ?? 0)}\n icon={<Brain className=\"h-5 w-5\" />}\n description=\"Your deepest uninterrupted stretch\"\n />\n </div>\n\n {/* Category breakdown — also the drop targets for drag-to-recategorize */}\n {segments.length > 0 && (\n <GlassCard treatment=\"glass\" className=\"p-4 sm:p-6\">\n <h2 className=\"mb-1 text-sm font-semibold text-text-primary\">\n Where the time went\n </h2>\n <p className=\"mb-4 text-xs text-text-secondary\">\n Drag a segment from the timeline below onto a category to\n recategorize it — or use its dropdown.\n </p>\n <div className=\"flex flex-wrap gap-4\">\n {CATEGORY_ORDER.filter((c) => totalsByCategory.has(c)).map(\n (category) => {\n const meta = CATEGORY_META[category];\n const total = totalsByCategory.get(category)!;\n return (\n <DroppableCategoryChip\n key={category}\n category={category}\n label={meta.label}\n dotClassName={meta.dot}\n totalSeconds={total.totalSeconds}\n />\n );\n },\n )}\n </div>\n </GlassCard>\n )}\n\n {/* Chronological timeline */}\n {segments.length === 0 ? (\n <EmptyState\n illustration=\"empty-generic\"\n title=\"No activity captured for this day\"\n description=\"As you track time, hold meetings, run focus sessions, and connect shared integrations, your day assembles here automatically — categorized and in order.\"\n />\n ) : (\n <GlassCard treatment=\"glass\" glow className=\"overflow-hidden\">\n <ol className=\"divide-y divide-border-seam\">\n {segments.map((segment: TimelineSegmentFieldsFragment) => (\n <DraggableSegmentRow\n key={segment.id}\n segment={segment}\n category={(segment.category as Category) ?? \"OTHER\"}\n projectName={\n segment.projectId\n ? projectNameById.get(segment.projectId)\n : undefined\n }\n recategorizing={recategorizing}\n onRecategorize={handleRecategorize}\n />\n ))}\n </ol>\n </GlassCard>\n )}\n </div>\n\n <DragOverlay>\n {draggingSegment && (\n <div className=\"flex items-center gap-2 rounded-lg border border-border-seam bg-bg-surface px-3 py-2 shadow-pop\">\n <GripVertical className=\"h-4 w-4 text-text-tertiary\" />\n <span className=\"text-sm font-medium text-text-primary\">\n {draggingSegment.title ||\n CATEGORY_META[\n (draggingSegment.category as Category) ?? \"OTHER\"\n ].label}\n </span>\n </div>\n )}\n </DragOverlay>\n </DndContext>\n )}\n </PageLayout>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"TimelinePage.js","names":[],"sources":["../../../../src/timecampus/pages/timeline/TimelinePage.tsx"],"sourcesContent":["import { useMemo, useState } from \"react\";\nimport {\n DndContext,\n DragOverlay,\n KeyboardSensor,\n PointerSensor,\n useDraggable,\n useDroppable,\n useSensor,\n useSensors,\n type DragEndEvent,\n type DragStartEvent,\n} from \"@dnd-kit/core\";\nimport {\n Activity,\n Brain,\n Calendar,\n Coffee,\n FolderOpen,\n Github,\n GripVertical,\n MessageSquare,\n Users,\n Zap,\n} from \"lucide-react\";\nimport { GlassCard } from \"@burdenoff/fe-libs/ui\";\nimport { PageLayout } from \"../../components/PageLayout\";\nimport { StatCard } from \"../../components/StatCard\";\nimport { EmptyState } from \"../../components/EmptyState\";\nimport { LoadingSkeleton } from \"../../components/LoadingSkeleton\";\nimport { formatDuration, formatTime } from \"../../utils/helpers\";\nimport { useTimeCampus } from \"../../providers/TimeCampusProvider\";\nimport {\n useGetTimeCampusTimelineQuery,\n useListProjectsQuery,\n useRecategorizeTimeCampusCaptureEventMutation,\n type TimelineSegmentFieldsFragment,\n} from \"@/generated/wspace-operations\";\n\ntype Category = \"MEETING\" | \"DEEP_WORK\" | \"COMMUNICATION\" | \"BREAK\" | \"OTHER\";\n\nconst CATEGORY_META: Record<\n Category,\n { label: string; icon: typeof Brain; dot: string; chip: string }\n> = {\n DEEP_WORK: {\n label: \"Deep work\",\n icon: Brain,\n dot: \"bg-status-success-bg\",\n chip: \"bg-status-success-bg text-status-success-text\",\n },\n MEETING: {\n label: \"Meeting\",\n icon: Users,\n dot: \"bg-status-info-bg\",\n chip: \"bg-status-info-bg text-status-info-text\",\n },\n COMMUNICATION: {\n label: \"Communication\",\n icon: MessageSquare,\n dot: \"bg-status-warning-bg\",\n chip: \"bg-status-warning-bg text-status-warning-text\",\n },\n BREAK: {\n label: \"Break\",\n icon: Coffee,\n dot: \"bg-bg-sunken\",\n chip: \"bg-bg-sunken text-text-secondary\",\n },\n OTHER: {\n label: \"Other\",\n icon: Activity,\n dot: \"bg-bg-sunken\",\n chip: \"bg-bg-sunken text-text-secondary\",\n },\n};\n\nconst SOURCE_ICON: Record<string, typeof Brain> = {\n TIME_ENTRY: Zap,\n MEETING: Users,\n POMODORO: Brain,\n EMAIL: MessageSquare,\n CAPTURE: Github,\n};\n\nconst CATEGORY_ORDER: Category[] = [\n \"DEEP_WORK\",\n \"MEETING\",\n \"COMMUNICATION\",\n \"BREAK\",\n \"OTHER\",\n];\n\n/**\n * How a segment came by its category. The service writes these lowercase\n * (`timeline.service.ts` → 'auto' | 'user' | 'channel'), so normalise before\n * matching rather than trusting the string's casing.\n */\nconst CATEGORY_SOURCE_LABEL: Record<string, string> = {\n auto: \"Auto-categorized\",\n user: \"You set this\",\n channel: \"From channel rules\",\n};\n\nfunction categorySourceLabel(source: string | null | undefined): string | null {\n if (!source) return null;\n return CATEGORY_SOURCE_LABEL[source.toLowerCase()] ?? source;\n}\n\n/** Droppable target: a category chip in the \"Where the time went\" summary. */\nfunction DroppableCategoryChip({\n category,\n label,\n dotClassName,\n totalSeconds,\n}: {\n category: Category;\n label: string;\n dotClassName: string;\n totalSeconds: number;\n}) {\n const { isOver, setNodeRef } = useDroppable({ id: `category:${category}` });\n return (\n <div\n ref={setNodeRef}\n className={`flex items-center gap-2 rounded-lg border px-3 py-2 transition-colors ${\n isOver\n ? \"border-action-primary-bg bg-action-primary-bg/10\"\n : \"border-border-seam bg-bg-surface\"\n }`}\n >\n <span\n className={`inline-block h-2.5 w-2.5 rounded-full ${dotClassName}`}\n aria-hidden\n />\n <span className=\"text-sm font-medium text-text-primary\">{label}</span>\n <span className=\"text-sm tabular-nums text-text-secondary\">\n {formatDuration(totalSeconds)}\n </span>\n {isOver && (\n <span className=\"text-xs font-medium text-primary\">Drop here</span>\n )}\n </div>\n );\n}\n\n/** Draggable row for a single timeline segment, with the existing dropdown kept as an accessible fallback. */\nfunction DraggableSegmentRow({\n segment,\n category,\n projectName,\n recategorizing,\n onRecategorize,\n}: {\n segment: TimelineSegmentFieldsFragment;\n category: Category;\n /** Resolved name for `segment.projectId`; undefined when it carries none. */\n projectName: string | undefined;\n recategorizing: boolean;\n onRecategorize: (\n segment: TimelineSegmentFieldsFragment,\n category: Category,\n ) => void;\n}) {\n const meta = CATEGORY_META[category] ?? CATEGORY_META.OTHER;\n const SourceIcon = SOURCE_ICON[segment.source] ?? Activity;\n const provenance = categorySourceLabel(segment.categorySource);\n const { attributes, listeners, setNodeRef, isDragging } = useDraggable({\n id: segment.id,\n data: { segment },\n disabled: !segment.isRecategorizable,\n });\n\n return (\n <li\n ref={setNodeRef}\n className={`flex items-start gap-3 p-4 transition-colors hover:bg-bg-sunken/50 ${\n isDragging ? \"opacity-40\" : \"\"\n }`}\n >\n <div className=\"flex w-16 shrink-0 flex-col items-end pt-0.5 text-xs tabular-nums text-text-secondary\">\n <span>{formatTime(segment.startTime)}</span>\n <span className=\"text-text-tertiary\">\n {formatDuration(segment.durationSeconds)}\n </span>\n </div>\n {segment.isRecategorizable && (\n <button\n type=\"button\"\n aria-label={`Drag to recategorize ${segment.title || meta.label}`}\n className=\"mt-1.5 shrink-0 cursor-grab touch-none text-text-tertiary hover:text-text-secondary active:cursor-grabbing\"\n {...attributes}\n {...listeners}\n >\n <GripVertical className=\"h-4 w-4\" />\n </button>\n )}\n <div\n className={`mt-0.5 flex h-8 w-8 shrink-0 items-center justify-center rounded-lg ${meta.chip}`}\n >\n <SourceIcon className=\"h-4 w-4\" />\n </div>\n <div className=\"min-w-0 flex-1\">\n <p className=\"truncate text-sm font-medium text-text-primary\">\n {segment.title || meta.label}\n </p>\n <div className=\"mt-1 flex flex-wrap items-center gap-2\">\n <span\n className={`inline-flex rounded-full px-2 py-0.5 text-xs font-medium ${meta.chip}`}\n >\n {meta.label}\n </span>\n {projectName !== undefined && (\n <span className=\"inline-flex max-w-[12rem] items-center gap-1 rounded-full bg-bg-sunken px-2 py-0.5 text-xs font-medium text-text-secondary\">\n <FolderOpen className=\"h-3 w-3 shrink-0\" />\n <span className=\"truncate\">{projectName}</span>\n </span>\n )}\n {segment.app && (\n <span className=\"text-xs text-text-secondary\">{segment.app}</span>\n )}\n {provenance && (\n <span className=\"text-xs text-text-tertiary\">{provenance}</span>\n )}\n </div>\n </div>\n {segment.isRecategorizable && (\n <select\n aria-label=\"Recategorize activity\"\n disabled={recategorizing}\n value={category}\n onChange={(e) => onRecategorize(segment, e.target.value as Category)}\n className=\"shrink-0 rounded-md border border-border-seam bg-bg-surface px-2 py-1 text-xs text-text-secondary focus:outline-none focus:ring-2 focus:ring-action-primary-bg/40\"\n >\n {CATEGORY_ORDER.map((c) => (\n <option key={c} value={c}>\n {CATEGORY_META[c].label}\n </option>\n ))}\n </select>\n )}\n </li>\n );\n}\n\nfunction toLocalDateInput(date: Date): string {\n const offset = date.getTimezoneOffset();\n const local = new Date(date.getTime() - offset * 60 * 1000);\n return local.toISOString().slice(0, 10);\n}\n\nfunction dayBounds(dateStr: string): { from: string; to: string } {\n const start = new Date(`${dateStr}T00:00:00`);\n const end = new Date(`${dateStr}T23:59:59.999`);\n return { from: start.toISOString(), to: end.toISOString() };\n}\n\nexport function TimelinePage() {\n const { workspaceId = \"\" } = useTimeCampus();\n const [dateStr, setDateStr] = useState(() => toLocalDateInput(new Date()));\n const [draggingSegment, setDraggingSegment] =\n useState<TimelineSegmentFieldsFragment | null>(null);\n\n const { from, to } = useMemo(() => dayBounds(dateStr), [dateStr]);\n\n const { data, loading, error, refetch } = useGetTimeCampusTimelineQuery({\n variables: { from, to },\n skip: !workspaceId,\n fetchPolicy: \"cache-and-network\",\n });\n\n // Segments carry a `projectId` only; the workspace's projects supply the\n // name to show next to them. A segment whose id isn't in this list simply\n // keeps no project chip — it is never labelled from a guess.\n const { data: projectsData } = useListProjectsQuery({\n variables: { pagination: { limit: 200 } },\n skip: !workspaceId,\n });\n\n const [recategorize, { loading: recategorizing }] =\n useRecategorizeTimeCampusCaptureEventMutation();\n\n const sensors = useSensors(\n useSensor(PointerSensor, { activationConstraint: { distance: 6 } }),\n useSensor(KeyboardSensor),\n );\n\n const timeline = data?.timeCampusTimeline;\n const segments = timeline?.segments ?? [];\n const summary = timeline?.summary;\n\n const projectNameById = useMemo(() => {\n const map = new Map<string, string>();\n for (const project of projectsData?.projects?.items ?? []) {\n map.set(project.id, project.name);\n }\n return map;\n }, [projectsData?.projects?.items]);\n\n const totalsByCategory = useMemo(() => {\n const map = new Map<\n string,\n { totalSeconds: number; segmentCount: number }\n >();\n for (const t of summary?.byCategory ?? []) {\n map.set(t.category, {\n totalSeconds: t.totalSeconds,\n segmentCount: t.segmentCount,\n });\n }\n return map;\n }, [summary]);\n\n const handleRecategorize = async (\n segment: TimelineSegmentFieldsFragment,\n category: Category,\n ) => {\n if (!segment.isRecategorizable || segment.category === category) return;\n await recategorize({\n variables: { id: segment.sourceId, category },\n });\n await refetch();\n };\n\n const handleDragStart = (event: DragStartEvent) => {\n const segment = event.active.data.current?.segment as\n TimelineSegmentFieldsFragment | undefined;\n setDraggingSegment(segment ?? null);\n };\n\n const handleDragEnd = (event: DragEndEvent) => {\n const segment = event.active.data.current?.segment as\n TimelineSegmentFieldsFragment | undefined;\n setDraggingSegment(null);\n const overId = event.over?.id;\n if (!segment || typeof overId !== \"string\") return;\n const category = overId.replace(/^category:/, \"\") as Category;\n if (!CATEGORY_ORDER.includes(category)) return;\n void handleRecategorize(segment, category);\n };\n\n const focusPct =\n summary && summary.focusRatio != null\n ? Math.round(summary.focusRatio * 100)\n : 0;\n\n // The real span of the day, straight off the summary — not inferred from the\n // segment list, which may be paginated or filtered differently.\n const daySpan =\n summary?.firstActivityAt && summary?.lastActivityAt\n ? `${formatTime(summary.firstActivityAt)} – ${formatTime(summary.lastActivityAt)}`\n : null;\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 <div className=\"flex flex-wrap items-center gap-3\">\n {daySpan && (\n <span className=\"inline-flex items-center gap-1.5 rounded-full bg-bg-sunken px-2.5 py-1 text-xs font-medium tabular-nums text-text-secondary\">\n <Activity className=\"h-3.5 w-3.5\" />\n {daySpan}\n </span>\n )}\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 </div>\n }\n >\n {loading && !timeline ? (\n <div className=\"space-y-6\">\n <LoadingSkeleton variant=\"card\" count={4} />\n <LoadingSkeleton variant=\"list\" count={6} />\n </div>\n ) : error ? (\n <div className=\"rounded-lg border border-status-error-bg/40 bg-status-error-bg p-4 text-sm text-status-error-text\">\n Failed to load your timeline. Please try again later.\n </div>\n ) : (\n <DndContext\n sensors={sensors}\n onDragStart={handleDragStart}\n onDragEnd={handleDragEnd}\n >\n <div className=\"space-y-6\">\n {/* Summary */}\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4\">\n <StatCard\n featured\n title=\"Tracked time\"\n value={formatDuration(summary?.totalTrackedSeconds ?? 0)}\n icon={<Activity className=\"h-5 w-5\" />}\n description=\"Total activity across the day\"\n />\n <StatCard\n title=\"Focus ratio\"\n value={`${focusPct}%`}\n icon={<Brain className=\"h-5 w-5\" />}\n description=\"Share of time in deep work\"\n />\n <StatCard\n title=\"Context switches\"\n value={summary?.contextSwitches ?? 0}\n icon={<Zap className=\"h-5 w-5\" />}\n description=\"Category changes through the day\"\n />\n <StatCard\n title=\"Longest focus block\"\n value={formatDuration(summary?.longestFocusBlockSeconds ?? 0)}\n icon={<Brain className=\"h-5 w-5\" />}\n description=\"Your deepest uninterrupted stretch\"\n />\n </div>\n\n {/* Category breakdown — also the drop targets for drag-to-recategorize */}\n {segments.length > 0 && (\n <GlassCard treatment=\"glass\" className=\"p-4 sm:p-6\">\n <h2 className=\"mb-1 text-sm font-semibold text-text-primary\">\n Where the time went\n </h2>\n <p className=\"mb-4 text-xs text-text-secondary\">\n Drag a segment from the timeline below onto a category to\n recategorize it — or use its dropdown.\n </p>\n <div className=\"flex flex-wrap gap-4\">\n {CATEGORY_ORDER.map((category) => {\n const meta = CATEGORY_META[category];\n const total = totalsByCategory.get(category);\n return (\n <DroppableCategoryChip\n key={category}\n category={category}\n label={meta.label}\n dotClassName={meta.dot}\n totalSeconds={total?.totalSeconds ?? 0}\n />\n );\n })}\n </div>\n </GlassCard>\n )}\n\n {/* Chronological timeline */}\n {segments.length === 0 ? (\n <EmptyState\n illustration=\"empty-generic\"\n title=\"No activity captured for this day\"\n description=\"As you track time, hold meetings, run focus sessions, and connect shared integrations, your day assembles here automatically — categorized and in order.\"\n />\n ) : (\n <GlassCard treatment=\"glass\" glow className=\"overflow-hidden\">\n <ol className=\"divide-y divide-border-seam\">\n {segments.map((segment: TimelineSegmentFieldsFragment) => (\n <DraggableSegmentRow\n key={segment.id}\n segment={segment}\n category={(segment.category as Category) ?? \"OTHER\"}\n projectName={\n segment.projectId\n ? projectNameById.get(segment.projectId)\n : undefined\n }\n recategorizing={recategorizing}\n onRecategorize={handleRecategorize}\n />\n ))}\n </ol>\n </GlassCard>\n )}\n </div>\n\n <DragOverlay>\n {draggingSegment && (\n <div className=\"flex items-center gap-2 rounded-lg border border-border-seam bg-bg-surface px-3 py-2 shadow-pop\">\n <GripVertical className=\"h-4 w-4 text-text-tertiary\" />\n <span className=\"text-sm font-medium text-text-primary\">\n {draggingSegment.title ||\n CATEGORY_META[\n (draggingSegment.category as Category) ?? \"OTHER\"\n ].label}\n </span>\n </div>\n )}\n </DragOverlay>\n </DndContext>\n )}\n </PageLayout>\n );\n}\n"],"mappings":"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"}
@@ -0,0 +1,17 @@
1
+ /**
2
+ * useTr — translation hook with fallback semantics.
3
+ *
4
+ * Wraps `useI18n().t()` so that when a key has no translation (i.e. the
5
+ * provider returns the key itself), the caller's English fallback is used
6
+ * instead. This keeps consumer call sites consistent and avoids leaking
7
+ * untranslated keys into the UI.
8
+ *
9
+ * Supports optional interpolation params using `{{key}}` placeholders.
10
+ *
11
+ * @example
12
+ * const tr = useTr();
13
+ * return <h1>{tr('timecampus.timer.title', 'Timer')}</h1>;
14
+ * return <p>{tr('timecampus.timer.activeCount', '{{count}} active', { count: 5 })}</p>;
15
+ */
16
+ export declare function useTr(): (key: string, fallback: string, params?: Record<string, string | number>) => string;
17
+ //# sourceMappingURL=useTr.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useTr.d.ts","sourceRoot":"","sources":["../../../../src/timecampus/shared/hooks/useTr.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,KAAK,IAAI,CACvB,GAAG,EAAE,MAAM,EACX,QAAQ,EAAE,MAAM,EAChB,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,KACrC,MAAM,CAmBV"}
@@ -0,0 +1,14 @@
1
+ import { useCallback as e } from "react";
2
+ import { useI18n as t } from "@burdenoff/fe-libs/shared/providers/shell/I18nProvider";
3
+ //#region src/timecampus/shared/hooks/useTr.ts
4
+ function n() {
5
+ let { t: n } = t();
6
+ return e((e, t, r) => {
7
+ let i = n(e), a = i === e ? t : i;
8
+ return r ? Object.entries(r).reduce((e, [t, n]) => e.replace(RegExp(`\\{\\{${t}\\}\\}`, "g"), String(n)), a) : a;
9
+ }, [n]);
10
+ }
11
+ //#endregion
12
+ export { n as useTr };
13
+
14
+ //# sourceMappingURL=useTr.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useTr.js","names":[],"sources":["../../../../src/timecampus/shared/hooks/useTr.ts"],"sourcesContent":["import { useCallback } from \"react\";\nimport { useI18n } from \"@burdenoff/fe-libs/shared/providers/shell/I18nProvider\";\n\n/**\n * useTr — translation hook with fallback semantics.\n *\n * Wraps `useI18n().t()` so that when a key has no translation (i.e. the\n * provider returns the key itself), the caller's English fallback is used\n * instead. This keeps consumer call sites consistent and avoids leaking\n * untranslated keys into the UI.\n *\n * Supports optional interpolation params using `{{key}}` placeholders.\n *\n * @example\n * const tr = useTr();\n * return <h1>{tr('timecampus.timer.title', 'Timer')}</h1>;\n * return <p>{tr('timecampus.timer.activeCount', '{{count}} active', { count: 5 })}</p>;\n */\nexport function useTr(): (\n key: string,\n fallback: string,\n params?: Record<string, string | number>,\n) => string {\n const { t } = useI18n();\n return useCallback(\n (\n key: string,\n fallback: string,\n params?: Record<string, string | number>,\n ): string => {\n const translated = t(key);\n const raw = translated === key ? fallback : translated;\n if (!params) return raw;\n return Object.entries(params).reduce(\n (str, [k, v]) =>\n str.replace(new RegExp(`\\\\{\\\\{${k}\\\\}\\\\}`, \"g\"), String(v)),\n raw,\n );\n },\n [t],\n );\n}\n"],"mappings":";;;AAkBA,SAAgB,IAIJ;CACV,IAAM,EAAE,SAAM,EAAQ;CACtB,OAAO,GAEH,GACA,GACA,MACW;EACX,IAAM,IAAa,EAAE,CAAG,GAClB,IAAM,MAAe,IAAM,IAAW;EAE5C,OADK,IACE,OAAO,QAAQ,CAAM,CAAC,CAAC,QAC3B,GAAK,CAAC,GAAG,OACR,EAAI,QAAY,OAAO,SAAS,EAAE,SAAS,GAAG,GAAG,OAAO,CAAC,CAAC,GAC5D,CACF,IALoB;CAMtB,GACA,CAAC,CAAC,CACJ;AACF"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@burdenoff/microfe-timecampus",
3
- "version": "2026.827.1",
3
+ "version": "2026.830.1",
4
4
  "description": "Burdenoff TimeCampus Microfrontend - AI-powered time management and productivity platform",
5
5
  "type": "module",
6
6
  "files": [
@@ -125,7 +125,7 @@
125
125
  "@hono/node-server": ">=1.19.15",
126
126
  "ip-address": ">=10.3.1",
127
127
  "dompurify": ">=3.4.13",
128
- "react-router": ">=7.18.2 <9.0.0",
128
+ "react-router": ">=8.3.0 <9.0.0",
129
129
  "uuid": ">=11.1.1",
130
130
  "xlsx": ">=0.19.3",
131
131
  "vitest": ">=3.2.6 <4.0.0",