@burdenoff/microfe-timecampus 2026.626.2 → 2026.703.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. package/dist/generated/wspace-operations.js +996 -816
  2. package/dist/generated/wspace-operations.js.map +1 -1
  3. package/dist/generated/wspace-types.js +28 -0
  4. package/dist/generated/wspace-types.js.map +1 -0
  5. package/dist/index.js +4 -4
  6. package/dist/timecampus/TimeCampusRoutes.js +16 -12
  7. package/dist/timecampus/TimeCampusRoutes.js.map +1 -1
  8. package/dist/timecampus/components/Modal.js +65 -0
  9. package/dist/timecampus/components/Modal.js.map +1 -0
  10. package/dist/timecampus/components/StatusBadge.js +28 -0
  11. package/dist/timecampus/components/StatusBadge.js.map +1 -0
  12. package/dist/timecampus/components/Toast.js +87 -0
  13. package/dist/timecampus/components/Toast.js.map +1 -0
  14. package/dist/timecampus/index.js +1 -1
  15. package/dist/timecampus/pages/HomePage.js +143 -135
  16. package/dist/timecampus/pages/HomePage.js.map +1 -1
  17. package/dist/timecampus/pages/applets/AppletStorePage.js +14 -19
  18. package/dist/timecampus/pages/applets/AppletStorePage.js.map +1 -1
  19. package/dist/timecampus/pages/applets/InstalledAppletsPage.js +49 -37
  20. package/dist/timecampus/pages/applets/InstalledAppletsPage.js.map +1 -1
  21. package/dist/timecampus/pages/application-usage/ApplicationUsagePage.js +315 -87
  22. package/dist/timecampus/pages/application-usage/ApplicationUsagePage.js.map +1 -1
  23. package/dist/timecampus/pages/channels/ChannelsPage.js +340 -46
  24. package/dist/timecampus/pages/channels/ChannelsPage.js.map +1 -1
  25. package/dist/timecampus/pages/client-portal/ClientPortalPage.js +374 -92
  26. package/dist/timecampus/pages/client-portal/ClientPortalPage.js.map +1 -1
  27. package/dist/timecampus/pages/dashboard/ReportsPage.js +70 -37
  28. package/dist/timecampus/pages/dashboard/ReportsPage.js.map +1 -1
  29. package/dist/timecampus/pages/email-tracking/EmailTrackingPage.js +366 -60
  30. package/dist/timecampus/pages/email-tracking/EmailTrackingPage.js.map +1 -1
  31. package/dist/timecampus/pages/expense-tracking/ExpenseTrackingPage.js +461 -87
  32. package/dist/timecampus/pages/expense-tracking/ExpenseTrackingPage.js.map +1 -1
  33. package/dist/timecampus/pages/geofencing/GeofencingPage.js +314 -55
  34. package/dist/timecampus/pages/geofencing/GeofencingPage.js.map +1 -1
  35. package/dist/timecampus/pages/industry-solutions/IndustrySolutionsPage.js +30 -27
  36. package/dist/timecampus/pages/industry-solutions/IndustrySolutionsPage.js.map +1 -1
  37. package/dist/timecampus/pages/location-tracking/LocationTrackingPage.js +269 -74
  38. package/dist/timecampus/pages/location-tracking/LocationTrackingPage.js.map +1 -1
  39. package/dist/timecampus/pages/meetings/ActionItemFormModal.js +127 -0
  40. package/dist/timecampus/pages/meetings/ActionItemFormModal.js.map +1 -0
  41. package/dist/timecampus/pages/meetings/MeetingCreatePage.js +62 -52
  42. package/dist/timecampus/pages/meetings/MeetingCreatePage.js.map +1 -1
  43. package/dist/timecampus/pages/meetings/MeetingDetailPage.js +361 -74
  44. package/dist/timecampus/pages/meetings/MeetingDetailPage.js.map +1 -1
  45. package/dist/timecampus/pages/meetings/MeetingFormModal.js +156 -0
  46. package/dist/timecampus/pages/meetings/MeetingFormModal.js.map +1 -0
  47. package/dist/timecampus/pages/meetings/MeetingsPage.js +175 -74
  48. package/dist/timecampus/pages/meetings/MeetingsPage.js.map +1 -1
  49. package/dist/timecampus/pages/meetings/ParticipantFormModal.js +105 -0
  50. package/dist/timecampus/pages/meetings/ParticipantFormModal.js.map +1 -0
  51. package/dist/timecampus/pages/pomodoro/PomodoroPage.js +307 -67
  52. package/dist/timecampus/pages/pomodoro/PomodoroPage.js.map +1 -1
  53. package/dist/timecampus/pages/projects/ProjectCreatePage.js +51 -48
  54. package/dist/timecampus/pages/projects/ProjectCreatePage.js.map +1 -1
  55. package/dist/timecampus/pages/projects/ProjectDetailPage.js +248 -51
  56. package/dist/timecampus/pages/projects/ProjectDetailPage.js.map +1 -1
  57. package/dist/timecampus/pages/projects/ProjectsPage.js +354 -87
  58. package/dist/timecampus/pages/projects/ProjectsPage.js.map +1 -1
  59. package/dist/timecampus/pages/projects/TasksPage.js +367 -65
  60. package/dist/timecampus/pages/projects/TasksPage.js.map +1 -1
  61. package/dist/timecampus/pages/resource-planning/ResourcePlanningPage.js +674 -104
  62. package/dist/timecampus/pages/resource-planning/ResourcePlanningPage.js.map +1 -1
  63. package/dist/timecampus/pages/team/TeamAnalyticsPage.js +46 -34
  64. package/dist/timecampus/pages/team/TeamAnalyticsPage.js.map +1 -1
  65. package/dist/timecampus/pages/team/TeamMembersPage.js +93 -69
  66. package/dist/timecampus/pages/team/TeamMembersPage.js.map +1 -1
  67. package/dist/timecampus/pages/team/TeamPage.js +4 -3
  68. package/dist/timecampus/pages/team/TeamPage.js.map +1 -1
  69. package/dist/timecampus/pages/templates/TemplateDetailPage.js +72 -53
  70. package/dist/timecampus/pages/templates/TemplateDetailPage.js.map +1 -1
  71. package/dist/timecampus/pages/templates/TemplatesPage.js +40 -35
  72. package/dist/timecampus/pages/templates/TemplatesPage.js.map +1 -1
  73. package/dist/timecampus/pages/time/TimeEntriesPage.js +484 -97
  74. package/dist/timecampus/pages/time/TimeEntriesPage.js.map +1 -1
  75. package/dist/timecampus/pages/time/TimeTrackingPage.js +210 -99
  76. package/dist/timecampus/pages/time/TimeTrackingPage.js.map +1 -1
  77. package/dist/timecampus/pages/time/TimerPage.js +229 -97
  78. package/dist/timecampus/pages/time/TimerPage.js.map +1 -1
  79. package/dist/timecampus/pages/time-approval/TimeApprovalPage.js +315 -110
  80. package/dist/timecampus/pages/time-approval/TimeApprovalPage.js.map +1 -1
  81. package/dist/timecampus/pages/timeline/TimelinePage.js +250 -0
  82. package/dist/timecampus/pages/timeline/TimelinePage.js.map +1 -0
  83. package/dist/timecampus/pages/widgets/WidgetDetailPage.js +158 -76
  84. package/dist/timecampus/pages/widgets/WidgetDetailPage.js.map +1 -1
  85. package/dist/timecampus/pages/widgets/WidgetsPage.js +181 -52
  86. package/dist/timecampus/pages/widgets/WidgetsPage.js.map +1 -1
  87. package/dist/timecampus/pages/workflows/WorkflowsPage.js +212 -61
  88. package/dist/timecampus/pages/workflows/WorkflowsPage.js.map +1 -1
  89. package/dist/timecampus/store/timecampusStore.js +13 -1
  90. package/dist/timecampus/store/timecampusStore.js.map +1 -1
  91. package/dist/timecampus/utils/helpers.js +20 -10
  92. package/dist/timecampus/utils/helpers.js.map +1 -1
  93. package/package.json +1 -1
@@ -1,67 +1,361 @@
1
- import { useTimeCampus as e } from "../../providers/TimeCampusProvider.js";
2
- import { getStatusColor as t } from "../../utils/helpers.js";
3
- import { LoadingSkeleton as n } from "../../components/LoadingSkeleton.js";
4
- import { useListChannelConfigsQuery as r, useListChannelStatusesQuery as i } from "../../../generated/wspace-operations.js";
5
- import { PageLayout as a } from "../../components/PageLayout.js";
6
- import { EmptyState as o } from "../../components/EmptyState.js";
7
- import { Wifi as s, WifiOff as c } from "lucide-react";
8
- import { jsx as l, jsxs as u } from "react/jsx-runtime";
1
+ import { LoadingSkeleton as e } from "../../components/LoadingSkeleton.js";
2
+ import { useCreateChannelConfigMutation as t, useDeleteChannelConfigMutation as n, useListChannelConfigsQuery as r, useListChannelStatusesQuery as i, useSyncChannelMutation as ee, useToggleChannelMutation as a, useUpdateChannelConfigMutation as te } from "../../../generated/wspace-operations.js";
3
+ import { PageLayout as o } from "../../components/PageLayout.js";
4
+ import { EmptyState as ne } from "../../components/EmptyState.js";
5
+ import { ChannelStatus as s, TimeCampusChannelType as c } from "../../../generated/wspace-types.js";
6
+ import { Modal as l } from "../../components/Modal.js";
7
+ import { StatusBadge as re } from "../../components/StatusBadge.js";
8
+ import { ToastContainer as ie, useToast as ae } from "../../components/Toast.js";
9
+ import { useState as u } from "react";
10
+ import { Pencil as oe, Plus as d, Power as f, RefreshCw as p, Trash2 as m, Wifi as h, WifiOff as g } from "lucide-react";
11
+ import { Fragment as _, jsx as v, jsxs as y } from "react/jsx-runtime";
9
12
  //#region src/timecampus/pages/channels/ChannelsPage.tsx
10
- function d() {
11
- let { workspaceId: d = "" } = e(), { data: f, loading: p, error: m } = r(), { data: h } = i(), g = f?.channelConfigs ?? [], _ = h?.channelStatuses ?? [], v = new Map(_.map((e) => [e.channelId, e]));
12
- return p ? /* @__PURE__ */ l(a, {
13
+ var b = 25, se = [
14
+ {
15
+ value: c.Calendar,
16
+ label: "Calendar"
17
+ },
18
+ {
19
+ value: c.Desktop,
20
+ label: "Desktop"
21
+ },
22
+ {
23
+ value: c.Browser,
24
+ label: "Browser"
25
+ },
26
+ {
27
+ value: c.Mobile,
28
+ label: "Mobile"
29
+ },
30
+ {
31
+ value: c.Ide,
32
+ label: "IDE"
33
+ },
34
+ {
35
+ value: c.Cli,
36
+ label: "CLI"
37
+ },
38
+ {
39
+ value: c.Api,
40
+ label: "API"
41
+ },
42
+ {
43
+ value: c.Custom,
44
+ label: "Custom"
45
+ }
46
+ ], x = "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", S = {
47
+ name: "",
48
+ type: c.Calendar,
49
+ syncInterval: ""
50
+ };
51
+ function C() {
52
+ let { toasts: C, addToast: w, dismissToast: ce } = ae(), { data: le, loading: ue, error: de, refetch: T, fetchMore: E } = r({
53
+ variables: {
54
+ limit: b,
55
+ offset: 0
56
+ },
57
+ notifyOnNetworkStatusChange: !0
58
+ }), { data: D } = i(), [O, { loading: k }] = t(), [A, { loading: j }] = te(), [M, { loading: N }] = n(), [P, { loading: F }] = a(), [I, { loading: L }] = ee(), R = le?.channelConfigs ?? [], z = D?.channelStatuses ?? [], [B, V] = u(!1), [H, fe] = u(!0), [pe, U] = u(!1), [W, G] = u(null), [K, q] = u(S), [J, Y] = u(null), [me, X] = u(null), he = new Map(z.map((e) => [e.channelId, e])), ge = () => {
59
+ G(null), q(S), U(!0);
60
+ }, _e = (e) => {
61
+ G(e), q({
62
+ name: e.name,
63
+ type: Object.values(c).find((t) => t === e.type) ?? c.Custom,
64
+ syncInterval: e.syncInterval == null ? "" : String(e.syncInterval)
65
+ }), U(!0);
66
+ }, Z = () => {
67
+ U(!1), G(null);
68
+ }, Q = async () => {
69
+ if (!K.name.trim()) return;
70
+ let e = K.syncInterval ? Number(K.syncInterval) : void 0;
71
+ try {
72
+ W ? (await A({ variables: {
73
+ id: W.id,
74
+ input: {
75
+ name: K.name.trim(),
76
+ type: K.type,
77
+ syncInterval: e
78
+ }
79
+ } }), w({
80
+ title: "Channel updated",
81
+ variant: "success"
82
+ })) : (await O({ variables: { input: {
83
+ name: K.name.trim(),
84
+ type: K.type,
85
+ syncInterval: e,
86
+ status: s.Active
87
+ } } }), w({
88
+ title: "Channel added",
89
+ variant: "success"
90
+ })), Z(), await T();
91
+ } catch {
92
+ w({
93
+ title: W ? "Failed to update channel" : "Failed to add channel",
94
+ variant: "error"
95
+ });
96
+ }
97
+ }, ve = async () => {
98
+ if (J) try {
99
+ await M({ variables: { id: J.id } }), w({
100
+ title: "Channel deleted",
101
+ variant: "success"
102
+ }), Y(null), await T();
103
+ } catch {
104
+ w({
105
+ title: "Failed to delete channel",
106
+ variant: "error"
107
+ });
108
+ }
109
+ }, ye = async (e) => {
110
+ let t = e.status.toLowerCase() === "active";
111
+ X(e.id);
112
+ try {
113
+ await P({ variables: {
114
+ id: e.id,
115
+ status: t ? s.Inactive : s.Active
116
+ } }), w({
117
+ title: t ? "Channel disabled" : "Channel enabled",
118
+ variant: "success"
119
+ }), await T();
120
+ } catch {
121
+ w({
122
+ title: "Failed to toggle channel",
123
+ variant: "error"
124
+ });
125
+ } finally {
126
+ X(null);
127
+ }
128
+ }, be = async (e) => {
129
+ X(e.id);
130
+ try {
131
+ await I({ variables: { id: e.id } }), w({
132
+ title: "Sync started",
133
+ variant: "success"
134
+ }), await T();
135
+ } catch {
136
+ w({
137
+ title: "Failed to sync channel",
138
+ variant: "error"
139
+ });
140
+ } finally {
141
+ X(null);
142
+ }
143
+ }, xe = async () => {
144
+ V(!0);
145
+ try {
146
+ ((await E({
147
+ variables: {
148
+ limit: b,
149
+ offset: R.length
150
+ },
151
+ updateQuery: (e, { fetchMoreResult: t }) => t ? {
152
+ ...e,
153
+ channelConfigs: [...e.channelConfigs, ...t.channelConfigs]
154
+ } : e
155
+ })).data?.channelConfigs?.length ?? 0) < b && fe(!1);
156
+ } finally {
157
+ V(!1);
158
+ }
159
+ };
160
+ if (ue && R.length === 0) return /* @__PURE__ */ v(o, {
13
161
  title: "Channels",
14
162
  description: "Manage time tracking channels and integrations",
15
- children: /* @__PURE__ */ l(n, {
163
+ children: /* @__PURE__ */ v(e, {
16
164
  variant: "card",
17
165
  count: 6
18
166
  })
19
- }) : m ? /* @__PURE__ */ l(a, {
167
+ });
168
+ if (de) return /* @__PURE__ */ v(o, {
20
169
  title: "Channels",
21
170
  description: "Manage time tracking channels and integrations",
22
- children: /* @__PURE__ */ l("div", {
171
+ children: /* @__PURE__ */ v("div", {
23
172
  className: "rounded-lg border border-status-error-bg/40 bg-status-error-bg p-4 text-sm text-status-error-text",
24
173
  children: "Failed to load channels data. Please try again later."
25
174
  })
26
- }) : /* @__PURE__ */ l(a, {
175
+ });
176
+ let $ = /* @__PURE__ */ y("button", {
177
+ onClick: ge,
178
+ className: "inline-flex items-center gap-1.5 rounded-md bg-action-primary-bg px-3 py-2 text-sm font-medium text-action-primary-text hover:bg-action-primary-bg/90",
179
+ children: [/* @__PURE__ */ v(d, { className: "h-4 w-4" }), "Add Channel"]
180
+ });
181
+ return /* @__PURE__ */ y(o, {
27
182
  title: "Channels",
28
183
  description: "Manage time tracking channels and integrations",
29
184
  purpose: "Connect the tools and devices that feed time into TimeCampus — calendars, desktop trackers, integrations — and keep an eye on each connection's health so no work hours slip through the cracks.",
30
- children: g.length === 0 ? /* @__PURE__ */ l(o, {
31
- illustration: "empty-generic",
32
- title: "No channels configured yet",
33
- description: "Set up your first tracking channel to start pulling time and activity from your tools and devices."
34
- }) : /* @__PURE__ */ l("div", {
35
- className: "grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3",
36
- children: g.map((e) => {
37
- let n = v.get(e.id), r = e.status === "active", i = e.lastSync ? new Date(e.lastSync).toLocaleString() : "Never";
38
- return /* @__PURE__ */ u("div", {
39
- className: "rounded-xl border border-border-seam bg-gloss-card shadow-pop p-5 transition-colors hover:bg-bg-sunken/50 cursor-pointer",
40
- children: [/* @__PURE__ */ u("div", {
41
- className: "flex items-start justify-between",
42
- children: [/* @__PURE__ */ u("div", {
43
- className: "flex items-center gap-3",
44
- children: [r ? /* @__PURE__ */ l(s, { className: "h-5 w-5 text-status-success-text" }) : /* @__PURE__ */ l(c, { className: "h-5 w-5 text-text-secondary" }), /* @__PURE__ */ u("div", { children: [/* @__PURE__ */ l("p", {
45
- className: "text-sm font-semibold text-text-primary",
46
- children: e.name
47
- }), /* @__PURE__ */ l("p", {
48
- className: "text-xs text-text-secondary",
49
- children: e.type
50
- })] })]
51
- }), /* @__PURE__ */ l("span", {
52
- className: `inline-flex rounded-full px-2 py-0.5 text-xs font-medium ${t(e.status)}`,
53
- children: e.status
54
- })]
55
- }), /* @__PURE__ */ u("div", {
56
- className: "mt-3 flex items-center justify-between text-xs text-text-secondary",
57
- children: [/* @__PURE__ */ l("span", { children: n?.isHealthy ? "Healthy" : "Unhealthy" }), /* @__PURE__ */ u("span", { children: ["Sync: ", i] })]
185
+ actions: $,
186
+ children: [
187
+ R.length === 0 ? /* @__PURE__ */ v(ne, {
188
+ illustration: "empty-generic",
189
+ title: "No channels configured yet",
190
+ description: "Set up your first tracking channel to start pulling time and activity from your tools and devices.",
191
+ action: $
192
+ }) : /* @__PURE__ */ y(_, { children: [/* @__PURE__ */ v("div", {
193
+ className: "grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3",
194
+ children: R.map((e) => {
195
+ let t = he.get(e.id), n = e.status.toLowerCase() === "active", r = e.lastSync ? new Date(e.lastSync).toLocaleString() : "Never", i = me === e.id;
196
+ return /* @__PURE__ */ y("div", {
197
+ className: "flex flex-col rounded-xl border border-border-seam bg-gloss-card shadow-pop p-5 transition-colors",
198
+ children: [
199
+ /* @__PURE__ */ y("div", {
200
+ className: "flex items-start justify-between",
201
+ children: [/* @__PURE__ */ y("div", {
202
+ className: "flex items-center gap-3",
203
+ children: [n ? /* @__PURE__ */ v(h, { className: "h-5 w-5 text-status-success-text" }) : /* @__PURE__ */ v(g, { className: "h-5 w-5 text-text-secondary" }), /* @__PURE__ */ y("div", { children: [/* @__PURE__ */ v("p", {
204
+ className: "text-sm font-semibold text-text-primary",
205
+ children: e.name
206
+ }), /* @__PURE__ */ v("p", {
207
+ className: "text-xs text-text-secondary",
208
+ children: e.type
209
+ })] })]
210
+ }), /* @__PURE__ */ v(re, {
211
+ label: e.status,
212
+ status: e.status.toLowerCase()
213
+ })]
214
+ }),
215
+ /* @__PURE__ */ y("div", {
216
+ className: "mt-3 flex items-center justify-between text-xs text-text-secondary",
217
+ children: [/* @__PURE__ */ v("span", { children: t?.isHealthy ? "Healthy" : "Unhealthy" }), /* @__PURE__ */ y("span", { children: ["Sync: ", r] })]
218
+ }),
219
+ /* @__PURE__ */ y("div", {
220
+ className: "mt-4 flex items-center gap-1.5 border-t border-border-seam pt-3",
221
+ children: [
222
+ /* @__PURE__ */ y("button", {
223
+ type: "button",
224
+ disabled: i && L,
225
+ onClick: () => be(e),
226
+ title: "Sync now",
227
+ className: "inline-flex items-center gap-1 rounded-md border border-border-seam bg-bg-surface px-2 py-1 text-xs text-text-secondary hover:bg-bg-sunken/50 disabled:opacity-50",
228
+ children: [/* @__PURE__ */ v(p, { className: `h-3.5 w-3.5 ${i && L ? "animate-spin" : ""}` }), "Sync"]
229
+ }),
230
+ /* @__PURE__ */ y("button", {
231
+ type: "button",
232
+ disabled: i && F,
233
+ onClick: () => ye(e),
234
+ title: n ? "Disable channel" : "Enable channel",
235
+ className: "inline-flex items-center gap-1 rounded-md border border-border-seam bg-bg-surface px-2 py-1 text-xs text-text-secondary hover:bg-bg-sunken/50 disabled:opacity-50",
236
+ children: [/* @__PURE__ */ v(f, { className: "h-3.5 w-3.5" }), n ? "Disable" : "Enable"]
237
+ }),
238
+ /* @__PURE__ */ y("div", {
239
+ className: "ml-auto flex items-center gap-1",
240
+ children: [/* @__PURE__ */ v("button", {
241
+ type: "button",
242
+ onClick: () => _e(e),
243
+ title: "Edit channel",
244
+ className: "inline-flex items-center rounded-md border border-border-seam bg-bg-surface p-1.5 text-text-secondary hover:bg-bg-sunken/50",
245
+ children: /* @__PURE__ */ v(oe, { className: "h-3.5 w-3.5" })
246
+ }), /* @__PURE__ */ v("button", {
247
+ type: "button",
248
+ onClick: () => Y(e),
249
+ title: "Delete channel",
250
+ className: "inline-flex items-center rounded-md border border-border-seam bg-bg-surface p-1.5 text-status-error-text hover:bg-status-error-bg",
251
+ children: /* @__PURE__ */ v(m, { className: "h-3.5 w-3.5" })
252
+ })]
253
+ })
254
+ ]
255
+ })
256
+ ]
257
+ }, e.id);
258
+ })
259
+ }), H && R.length >= b && /* @__PURE__ */ v("div", {
260
+ className: "mt-4 flex justify-center",
261
+ children: /* @__PURE__ */ v("button", {
262
+ onClick: xe,
263
+ disabled: B,
264
+ className: "rounded-md border border-border-subtle px-4 py-2 text-sm font-medium text-text-primary hover:bg-bg-sunken/50 disabled:opacity-50",
265
+ children: B ? "Loading..." : "Load more"
266
+ })
267
+ })] }),
268
+ /* @__PURE__ */ v(l, {
269
+ open: pe,
270
+ onClose: Z,
271
+ title: W ? "Edit Channel" : "Add Channel",
272
+ description: W ? "Update this channel's configuration." : "Connect a new tracking channel to feed time into TimeCampus.",
273
+ footer: /* @__PURE__ */ y(_, { children: [/* @__PURE__ */ v("button", {
274
+ onClick: Z,
275
+ className: "rounded-md border border-border-subtle px-4 py-2 text-sm font-medium text-text-primary hover:bg-bg-sunken/50",
276
+ children: "Cancel"
277
+ }), /* @__PURE__ */ v("button", {
278
+ onClick: Q,
279
+ disabled: k || j || !K.name.trim(),
280
+ 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",
281
+ children: k || j ? "Saving..." : W ? "Save Changes" : "Add Channel"
282
+ })] }),
283
+ children: /* @__PURE__ */ y("div", {
284
+ className: "space-y-4",
285
+ children: [/* @__PURE__ */ y("div", { children: [/* @__PURE__ */ v("label", {
286
+ className: "block text-sm font-medium text-text-primary",
287
+ children: "Name"
288
+ }), /* @__PURE__ */ v("input", {
289
+ type: "text",
290
+ placeholder: "e.g. Google Calendar",
291
+ value: K.name,
292
+ onChange: (e) => q({
293
+ ...K,
294
+ name: e.target.value
295
+ }),
296
+ className: x
297
+ })] }), /* @__PURE__ */ y("div", {
298
+ className: "grid grid-cols-1 gap-4 sm:grid-cols-2",
299
+ children: [/* @__PURE__ */ y("div", { children: [/* @__PURE__ */ v("label", {
300
+ className: "block text-sm font-medium text-text-primary",
301
+ children: "Type"
302
+ }), /* @__PURE__ */ v("select", {
303
+ value: K.type,
304
+ onChange: (e) => q({
305
+ ...K,
306
+ type: e.target.value
307
+ }),
308
+ className: x,
309
+ children: se.map((e) => /* @__PURE__ */ v("option", {
310
+ value: e.value,
311
+ children: e.label
312
+ }, e.value))
313
+ })] }), /* @__PURE__ */ y("div", { children: [/* @__PURE__ */ v("label", {
314
+ className: "block text-sm font-medium text-text-primary",
315
+ children: "Sync Interval (seconds)"
316
+ }), /* @__PURE__ */ v("input", {
317
+ type: "number",
318
+ min: 0,
319
+ placeholder: "e.g. 300",
320
+ value: K.syncInterval,
321
+ onChange: (e) => q({
322
+ ...K,
323
+ syncInterval: e.target.value
324
+ }),
325
+ className: x
326
+ })] })]
58
327
  })]
59
- }, e.id);
328
+ })
329
+ }),
330
+ /* @__PURE__ */ v(l, {
331
+ open: J !== null,
332
+ onClose: () => Y(null),
333
+ title: "Delete channel?",
334
+ description: J ? `"${J.name}" will be removed. This cannot be undone.` : void 0,
335
+ size: "sm",
336
+ footer: /* @__PURE__ */ y(_, { children: [/* @__PURE__ */ v("button", {
337
+ onClick: () => Y(null),
338
+ className: "rounded-md border border-border-subtle px-4 py-2 text-sm font-medium text-text-primary hover:bg-bg-sunken/50",
339
+ children: "Cancel"
340
+ }), /* @__PURE__ */ v("button", {
341
+ onClick: ve,
342
+ disabled: N,
343
+ className: "rounded-md bg-status-error-bg px-4 py-2 text-sm font-medium text-status-error-text hover:bg-status-error-bg/80 disabled:opacity-50",
344
+ children: N ? "Deleting..." : "Delete"
345
+ })] }),
346
+ children: /* @__PURE__ */ v("p", {
347
+ className: "text-sm text-text-secondary",
348
+ children: "Deleting a channel stops it from feeding new time into TimeCampus. Previously captured entries are kept."
349
+ })
350
+ }),
351
+ /* @__PURE__ */ v(ie, {
352
+ toasts: C,
353
+ onDismiss: ce
60
354
  })
61
- })
355
+ ]
62
356
  });
63
357
  }
64
358
  //#endregion
65
- export { d as ChannelsPage };
359
+ export { C as ChannelsPage };
66
360
 
67
361
  //# sourceMappingURL=ChannelsPage.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"ChannelsPage.js","names":[],"sources":["../../../../src/timecampus/pages/channels/ChannelsPage.tsx"],"sourcesContent":["import { Wifi, WifiOff } from \"lucide-react\";\nimport { PageLayout } from \"../../components/PageLayout\";\nimport { EmptyState } from \"../../components/EmptyState\";\nimport { LoadingSkeleton } from \"../../components/LoadingSkeleton\";\nimport { getStatusColor } from \"../../utils/helpers\";\nimport { useTimeCampus } from \"../../providers/TimeCampusProvider\";\nimport {\n useListChannelConfigsQuery,\n useListChannelStatusesQuery,\n type ChannelConfigFieldsFragment,\n} from \"@/generated/wspace-operations\";\n\nexport function ChannelsPage() {\n const { workspaceId = \"\" } = useTimeCampus();\n\n const {\n data: configsData,\n loading: configsLoading,\n error: configsError,\n } = useListChannelConfigsQuery();\n const { data: statusesData } = useListChannelStatusesQuery();\n\n const channels = configsData?.channelConfigs ?? [];\n const statuses = statusesData?.channelStatuses ?? [];\n\n type ChannelStatusItem = {\n channelId: string;\n name: string;\n type: string;\n status: string;\n isHealthy: boolean;\n lastSync?: string | null;\n };\n\n // Build a lookup map for status info\n const statusMap = new Map<string, ChannelStatusItem>(\n statuses.map((s: ChannelStatusItem) => [s.channelId, s]),\n );\n\n if (configsLoading) {\n return (\n <PageLayout\n title=\"Channels\"\n description=\"Manage time tracking channels and integrations\"\n >\n <LoadingSkeleton variant=\"card\" count={6} />\n </PageLayout>\n );\n }\n\n if (configsError) {\n return (\n <PageLayout\n title=\"Channels\"\n description=\"Manage time tracking channels and integrations\"\n >\n <div className=\"rounded-lg border border-status-error-bg/40 bg-status-error-bg p-4 text-sm text-status-error-text\">\n Failed to load channels data. Please try again later.\n </div>\n </PageLayout>\n );\n }\n\n return (\n <PageLayout\n title=\"Channels\"\n description=\"Manage time tracking channels and integrations\"\n purpose=\"Connect the tools and devices that feed time into TimeCampus — calendars, desktop trackers, integrations — and keep an eye on each connection's health so no work hours slip through the cracks.\"\n >\n {channels.length === 0 ? (\n <EmptyState\n illustration=\"empty-generic\"\n title=\"No channels configured yet\"\n description=\"Set up your first tracking channel to start pulling time and activity from your tools and devices.\"\n />\n ) : (\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3\">\n {channels.map((channel: ChannelConfigFieldsFragment) => {\n const statusInfo = statusMap.get(channel.id);\n const isActive = channel.status === \"active\";\n const lastSync = channel.lastSync\n ? new Date(channel.lastSync).toLocaleString()\n : \"Never\";\n\n return (\n <div\n key={channel.id}\n className=\"rounded-xl border border-border-seam bg-gloss-card shadow-pop p-5 transition-colors hover:bg-bg-sunken/50 cursor-pointer\"\n >\n <div className=\"flex items-start justify-between\">\n <div className=\"flex items-center gap-3\">\n {isActive ? (\n <Wifi className=\"h-5 w-5 text-status-success-text\" />\n ) : (\n <WifiOff className=\"h-5 w-5 text-text-secondary\" />\n )}\n <div>\n <p className=\"text-sm font-semibold text-text-primary\">\n {channel.name}\n </p>\n <p className=\"text-xs text-text-secondary\">\n {channel.type}\n </p>\n </div>\n </div>\n <span\n className={`inline-flex rounded-full px-2 py-0.5 text-xs font-medium ${getStatusColor(channel.status)}`}\n >\n {channel.status}\n </span>\n </div>\n <div className=\"mt-3 flex items-center justify-between text-xs text-text-secondary\">\n <span>{statusInfo?.isHealthy ? \"Healthy\" : \"Unhealthy\"}</span>\n <span>Sync: {lastSync}</span>\n </div>\n </div>\n );\n })}\n </div>\n )}\n </PageLayout>\n );\n}\n"],"mappings":";;;;;;;;;AAYA,SAAgB,IAAe;CAC7B,IAAM,EAAE,iBAAc,OAAO,GAAe,EAEtC,EACJ,MAAM,GACN,SAAS,GACT,OAAO,MACL,GAA4B,EAC1B,EAAE,MAAM,MAAiB,GAA6B,EAEtD,IAAW,GAAa,kBAAkB,EAAE,EAC5C,IAAW,GAAc,mBAAmB,EAAE,EAY9C,IAAY,IAAI,IACpB,EAAS,KAAK,MAAyB,CAAC,EAAE,WAAW,EAAE,CAAC,CACzD;AA0BD,QAxBI,IAEA,kBAAC,GAAD;EACE,OAAM;EACN,aAAY;YAEZ,kBAAC,GAAD;GAAiB,SAAQ;GAAO,OAAO;GAAK,CAAA;EACjC,CAAA,GAIb,IAEA,kBAAC,GAAD;EACE,OAAM;EACN,aAAY;YAEZ,kBAAC,OAAD;GAAK,WAAU;aAAoG;GAE7G,CAAA;EACK,CAAA,GAKf,kBAAC,GAAD;EACE,OAAM;EACN,aAAY;EACZ,SAAQ;YAEP,EAAS,WAAW,IACnB,kBAAC,GAAD;GACE,cAAa;GACb,OAAM;GACN,aAAY;GACZ,CAAA,GAEF,kBAAC,OAAD;GAAK,WAAU;aACZ,EAAS,KAAK,MAAyC;IACtD,IAAM,IAAa,EAAU,IAAI,EAAQ,GAAG,EACtC,IAAW,EAAQ,WAAW,UAC9B,IAAW,EAAQ,WACrB,IAAI,KAAK,EAAQ,SAAS,CAAC,gBAAgB,GAC3C;AAEJ,WACE,kBAAC,OAAD;KAEE,WAAU;eAFZ,CAIE,kBAAC,OAAD;MAAK,WAAU;gBAAf,CACE,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACG,IACC,kBAAC,GAAD,EAAM,WAAU,oCAAqC,CAAA,GAErD,kBAAC,GAAD,EAAS,WAAU,+BAAgC,CAAA,EAErD,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,KAAD;QAAG,WAAU;kBACV,EAAQ;QACP,CAAA,EACJ,kBAAC,KAAD;QAAG,WAAU;kBACV,EAAQ;QACP,CAAA,CACA,EAAA,CAAA,CACF;UACN,kBAAC,QAAD;OACE,WAAW,4DAA4D,EAAe,EAAQ,OAAO;iBAEpG,EAAQ;OACJ,CAAA,CACH;SACN,kBAAC,OAAD;MAAK,WAAU;gBAAf,CACE,kBAAC,QAAD,EAAA,UAAO,GAAY,YAAY,YAAY,aAAmB,CAAA,EAC9D,kBAAC,QAAD,EAAA,UAAA,CAAM,UAAO,EAAgB,EAAA,CAAA,CACzB;QACF;OA7BC,EAAQ,GA6BT;KAER;GACE,CAAA;EAEG,CAAA"}
1
+ {"version":3,"file":"ChannelsPage.js","names":[],"sources":["../../../../src/timecampus/pages/channels/ChannelsPage.tsx"],"sourcesContent":["import { useState } from \"react\";\nimport {\n Wifi,\n WifiOff,\n Plus,\n RefreshCw,\n Pencil,\n Trash2,\n Power,\n} from \"lucide-react\";\nimport { PageLayout } from \"../../components/PageLayout\";\nimport { EmptyState } from \"../../components/EmptyState\";\nimport { LoadingSkeleton } from \"../../components/LoadingSkeleton\";\nimport { StatusBadge } from \"../../components/StatusBadge\";\nimport { Modal } from \"../../components/Modal\";\nimport { ToastContainer, useToast } from \"../../components/Toast\";\nimport {\n useListChannelConfigsQuery,\n useListChannelStatusesQuery,\n useCreateChannelConfigMutation,\n useUpdateChannelConfigMutation,\n useDeleteChannelConfigMutation,\n useToggleChannelMutation,\n useSyncChannelMutation,\n type ChannelConfigFieldsFragment,\n} from \"@/generated/wspace-operations\";\nimport { ChannelStatus, TimeCampusChannelType } from \"@/generated/wspace-types\";\n\nconst PAGE_SIZE = 25;\n\nconst CHANNEL_TYPE_OPTIONS: { value: TimeCampusChannelType; label: string }[] =\n [\n { value: TimeCampusChannelType.Calendar, label: \"Calendar\" },\n { value: TimeCampusChannelType.Desktop, label: \"Desktop\" },\n { value: TimeCampusChannelType.Browser, label: \"Browser\" },\n { value: TimeCampusChannelType.Mobile, label: \"Mobile\" },\n { value: TimeCampusChannelType.Ide, label: \"IDE\" },\n { value: TimeCampusChannelType.Cli, label: \"CLI\" },\n { value: TimeCampusChannelType.Api, label: \"API\" },\n { value: TimeCampusChannelType.Custom, label: \"Custom\" },\n ];\n\nconst inputClass =\n \"mt-1 w-full rounded-md border border-border-subtle bg-bg-surface px-3 py-2 text-sm text-text-primary placeholder:text-text-secondary\";\n\ninterface ChannelFormState {\n name: string;\n type: TimeCampusChannelType;\n syncInterval: string;\n}\n\nconst emptyForm: ChannelFormState = {\n name: \"\",\n type: TimeCampusChannelType.Calendar,\n syncInterval: \"\",\n};\n\nexport function ChannelsPage() {\n const { toasts, addToast, dismissToast } = useToast();\n\n const {\n data: configsData,\n loading: configsLoading,\n error: configsError,\n refetch,\n fetchMore,\n } = useListChannelConfigsQuery({\n variables: { limit: PAGE_SIZE, offset: 0 },\n notifyOnNetworkStatusChange: true,\n });\n const { data: statusesData } = useListChannelStatusesQuery();\n\n const [createChannel, { loading: creating }] =\n useCreateChannelConfigMutation();\n const [updateChannel, { loading: updating }] =\n useUpdateChannelConfigMutation();\n const [deleteChannel, { loading: deleting }] =\n useDeleteChannelConfigMutation();\n const [toggleChannel, { loading: toggling }] = useToggleChannelMutation();\n const [syncChannel, { loading: syncing }] = useSyncChannelMutation();\n\n const channels = configsData?.channelConfigs ?? [];\n const statuses = statusesData?.channelStatuses ?? [];\n\n const [loadingMore, setLoadingMore] = useState(false);\n const [canLoadMore, setCanLoadMore] = useState(true);\n\n // Create / edit modal state\n const [formOpen, setFormOpen] = useState(false);\n const [editing, setEditing] = useState<ChannelConfigFieldsFragment | null>(\n null,\n );\n const [form, setForm] = useState<ChannelFormState>(emptyForm);\n\n // Delete confirm state\n const [deleteTarget, setDeleteTarget] =\n useState<ChannelConfigFieldsFragment | null>(null);\n\n // Per-row action tracking (which channel id is mid-mutation)\n const [busyId, setBusyId] = useState<string | null>(null);\n\n type ChannelStatusItem = {\n channelId: string;\n name: string;\n type: string;\n status: string;\n isHealthy: boolean;\n lastSync?: string | null;\n };\n\n const statusMap = new Map<string, ChannelStatusItem>(\n statuses.map((s: ChannelStatusItem) => [s.channelId, s]),\n );\n\n const openCreate = () => {\n setEditing(null);\n setForm(emptyForm);\n setFormOpen(true);\n };\n\n const openEdit = (channel: ChannelConfigFieldsFragment) => {\n setEditing(channel);\n setForm({\n name: channel.name,\n type:\n (Object.values(TimeCampusChannelType).find(\n (t) => t === channel.type,\n ) as TimeCampusChannelType | undefined) ?? TimeCampusChannelType.Custom,\n syncInterval:\n channel.syncInterval != null ? String(channel.syncInterval) : \"\",\n });\n setFormOpen(true);\n };\n\n const closeForm = () => {\n setFormOpen(false);\n setEditing(null);\n };\n\n const handleSubmit = async () => {\n if (!form.name.trim()) return;\n const syncInterval = form.syncInterval\n ? Number(form.syncInterval)\n : undefined;\n try {\n if (editing) {\n await updateChannel({\n variables: {\n id: editing.id,\n input: {\n name: form.name.trim(),\n type: form.type,\n syncInterval,\n },\n },\n });\n addToast({ title: \"Channel updated\", variant: \"success\" });\n } else {\n await createChannel({\n variables: {\n input: {\n name: form.name.trim(),\n type: form.type,\n syncInterval,\n status: ChannelStatus.Active,\n },\n },\n });\n addToast({ title: \"Channel added\", variant: \"success\" });\n }\n closeForm();\n await refetch();\n } catch {\n addToast({\n title: editing ? \"Failed to update channel\" : \"Failed to add channel\",\n variant: \"error\",\n });\n }\n };\n\n const handleDelete = async () => {\n if (!deleteTarget) return;\n try {\n await deleteChannel({ variables: { id: deleteTarget.id } });\n addToast({ title: \"Channel deleted\", variant: \"success\" });\n setDeleteTarget(null);\n await refetch();\n } catch {\n addToast({ title: \"Failed to delete channel\", variant: \"error\" });\n }\n };\n\n const handleToggle = async (channel: ChannelConfigFieldsFragment) => {\n const isActive = channel.status.toLowerCase() === \"active\";\n setBusyId(channel.id);\n try {\n await toggleChannel({\n variables: {\n id: channel.id,\n status: isActive ? ChannelStatus.Inactive : ChannelStatus.Active,\n },\n });\n addToast({\n title: isActive ? \"Channel disabled\" : \"Channel enabled\",\n variant: \"success\",\n });\n await refetch();\n } catch {\n addToast({ title: \"Failed to toggle channel\", variant: \"error\" });\n } finally {\n setBusyId(null);\n }\n };\n\n const handleSync = async (channel: ChannelConfigFieldsFragment) => {\n setBusyId(channel.id);\n try {\n await syncChannel({ variables: { id: channel.id } });\n addToast({ title: \"Sync started\", variant: \"success\" });\n await refetch();\n } catch {\n addToast({ title: \"Failed to sync channel\", variant: \"error\" });\n } finally {\n setBusyId(null);\n }\n };\n\n const handleLoadMore = async () => {\n setLoadingMore(true);\n try {\n const res = await fetchMore({\n variables: { limit: PAGE_SIZE, offset: channels.length },\n updateQuery: (prev, { fetchMoreResult }) => {\n if (!fetchMoreResult) return prev;\n return {\n ...prev,\n channelConfigs: [\n ...prev.channelConfigs,\n ...fetchMoreResult.channelConfigs,\n ],\n };\n },\n });\n if ((res.data?.channelConfigs?.length ?? 0) < PAGE_SIZE) {\n setCanLoadMore(false);\n }\n } finally {\n setLoadingMore(false);\n }\n };\n\n if (configsLoading && channels.length === 0) {\n return (\n <PageLayout\n title=\"Channels\"\n description=\"Manage time tracking channels and integrations\"\n >\n <LoadingSkeleton variant=\"card\" count={6} />\n </PageLayout>\n );\n }\n\n if (configsError) {\n return (\n <PageLayout\n title=\"Channels\"\n description=\"Manage time tracking channels and integrations\"\n >\n <div className=\"rounded-lg border border-status-error-bg/40 bg-status-error-bg p-4 text-sm text-status-error-text\">\n Failed to load channels data. Please try again later.\n </div>\n </PageLayout>\n );\n }\n\n const addButton = (\n <button\n onClick={openCreate}\n className=\"inline-flex items-center gap-1.5 rounded-md bg-action-primary-bg px-3 py-2 text-sm font-medium text-action-primary-text hover:bg-action-primary-bg/90\"\n >\n <Plus className=\"h-4 w-4\" />\n Add Channel\n </button>\n );\n\n return (\n <PageLayout\n title=\"Channels\"\n description=\"Manage time tracking channels and integrations\"\n purpose=\"Connect the tools and devices that feed time into TimeCampus — calendars, desktop trackers, integrations — and keep an eye on each connection's health so no work hours slip through the cracks.\"\n actions={addButton}\n >\n {channels.length === 0 ? (\n <EmptyState\n illustration=\"empty-generic\"\n title=\"No channels configured yet\"\n description=\"Set up your first tracking channel to start pulling time and activity from your tools and devices.\"\n action={addButton}\n />\n ) : (\n <>\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3\">\n {channels.map((channel: ChannelConfigFieldsFragment) => {\n const statusInfo = statusMap.get(channel.id);\n const isActive = channel.status.toLowerCase() === \"active\";\n const lastSync = channel.lastSync\n ? new Date(channel.lastSync).toLocaleString()\n : \"Never\";\n const rowBusy = busyId === channel.id;\n\n return (\n <div\n key={channel.id}\n className=\"flex flex-col rounded-xl border border-border-seam bg-gloss-card shadow-pop p-5 transition-colors\"\n >\n <div className=\"flex items-start justify-between\">\n <div className=\"flex items-center gap-3\">\n {isActive ? (\n <Wifi className=\"h-5 w-5 text-status-success-text\" />\n ) : (\n <WifiOff className=\"h-5 w-5 text-text-secondary\" />\n )}\n <div>\n <p className=\"text-sm font-semibold text-text-primary\">\n {channel.name}\n </p>\n <p className=\"text-xs text-text-secondary\">\n {channel.type}\n </p>\n </div>\n </div>\n <StatusBadge\n label={channel.status}\n status={channel.status.toLowerCase()}\n />\n </div>\n <div className=\"mt-3 flex items-center justify-between text-xs text-text-secondary\">\n <span>\n {statusInfo?.isHealthy ? \"Healthy\" : \"Unhealthy\"}\n </span>\n <span>Sync: {lastSync}</span>\n </div>\n <div className=\"mt-4 flex items-center gap-1.5 border-t border-border-seam pt-3\">\n <button\n type=\"button\"\n disabled={rowBusy && syncing}\n onClick={() => handleSync(channel)}\n title=\"Sync now\"\n className=\"inline-flex items-center gap-1 rounded-md border border-border-seam bg-bg-surface px-2 py-1 text-xs text-text-secondary hover:bg-bg-sunken/50 disabled:opacity-50\"\n >\n <RefreshCw\n className={`h-3.5 w-3.5 ${rowBusy && syncing ? \"animate-spin\" : \"\"}`}\n />\n Sync\n </button>\n <button\n type=\"button\"\n disabled={rowBusy && toggling}\n onClick={() => handleToggle(channel)}\n title={isActive ? \"Disable channel\" : \"Enable channel\"}\n className=\"inline-flex items-center gap-1 rounded-md border border-border-seam bg-bg-surface px-2 py-1 text-xs text-text-secondary hover:bg-bg-sunken/50 disabled:opacity-50\"\n >\n <Power className=\"h-3.5 w-3.5\" />\n {isActive ? \"Disable\" : \"Enable\"}\n </button>\n <div className=\"ml-auto flex items-center gap-1\">\n <button\n type=\"button\"\n onClick={() => openEdit(channel)}\n title=\"Edit channel\"\n className=\"inline-flex items-center rounded-md border border-border-seam bg-bg-surface p-1.5 text-text-secondary hover:bg-bg-sunken/50\"\n >\n <Pencil className=\"h-3.5 w-3.5\" />\n </button>\n <button\n type=\"button\"\n onClick={() => setDeleteTarget(channel)}\n title=\"Delete channel\"\n className=\"inline-flex items-center rounded-md border border-border-seam bg-bg-surface p-1.5 text-status-error-text hover:bg-status-error-bg\"\n >\n <Trash2 className=\"h-3.5 w-3.5\" />\n </button>\n </div>\n </div>\n </div>\n );\n })}\n </div>\n\n {canLoadMore && channels.length >= PAGE_SIZE && (\n <div className=\"mt-4 flex justify-center\">\n <button\n onClick={handleLoadMore}\n disabled={loadingMore}\n className=\"rounded-md border border-border-subtle px-4 py-2 text-sm font-medium text-text-primary hover:bg-bg-sunken/50 disabled:opacity-50\"\n >\n {loadingMore ? \"Loading...\" : \"Load more\"}\n </button>\n </div>\n )}\n </>\n )}\n\n <Modal\n open={formOpen}\n onClose={closeForm}\n title={editing ? \"Edit Channel\" : \"Add Channel\"}\n description={\n editing\n ? \"Update this channel's configuration.\"\n : \"Connect a new tracking channel to feed time into TimeCampus.\"\n }\n footer={\n <>\n <button\n onClick={closeForm}\n className=\"rounded-md border border-border-subtle px-4 py-2 text-sm font-medium text-text-primary hover:bg-bg-sunken/50\"\n >\n Cancel\n </button>\n <button\n onClick={handleSubmit}\n disabled={creating || updating || !form.name.trim()}\n className=\"rounded-md bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text hover:bg-action-primary-bg/90 disabled:opacity-50\"\n >\n {creating || updating\n ? \"Saving...\"\n : editing\n ? \"Save Changes\"\n : \"Add Channel\"}\n </button>\n </>\n }\n >\n <div className=\"space-y-4\">\n <div>\n <label className=\"block text-sm font-medium text-text-primary\">\n Name\n </label>\n <input\n type=\"text\"\n placeholder=\"e.g. Google Calendar\"\n value={form.name}\n onChange={(e) => setForm({ ...form, name: e.target.value })}\n className={inputClass}\n />\n </div>\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2\">\n <div>\n <label className=\"block text-sm font-medium text-text-primary\">\n Type\n </label>\n <select\n value={form.type}\n onChange={(e) =>\n setForm({\n ...form,\n type: e.target.value as TimeCampusChannelType,\n })\n }\n className={inputClass}\n >\n {CHANNEL_TYPE_OPTIONS.map((opt) => (\n <option key={opt.value} value={opt.value}>\n {opt.label}\n </option>\n ))}\n </select>\n </div>\n <div>\n <label className=\"block text-sm font-medium text-text-primary\">\n Sync Interval (seconds)\n </label>\n <input\n type=\"number\"\n min={0}\n placeholder=\"e.g. 300\"\n value={form.syncInterval}\n onChange={(e) =>\n setForm({ ...form, syncInterval: e.target.value })\n }\n className={inputClass}\n />\n </div>\n </div>\n </div>\n </Modal>\n\n <Modal\n open={deleteTarget !== null}\n onClose={() => setDeleteTarget(null)}\n title=\"Delete channel?\"\n description={\n deleteTarget\n ? `\"${deleteTarget.name}\" will be removed. This cannot be undone.`\n : undefined\n }\n size=\"sm\"\n footer={\n <>\n <button\n onClick={() => setDeleteTarget(null)}\n className=\"rounded-md border border-border-subtle px-4 py-2 text-sm font-medium text-text-primary hover:bg-bg-sunken/50\"\n >\n Cancel\n </button>\n <button\n onClick={handleDelete}\n disabled={deleting}\n className=\"rounded-md bg-status-error-bg px-4 py-2 text-sm font-medium text-status-error-text hover:bg-status-error-bg/80 disabled:opacity-50\"\n >\n {deleting ? \"Deleting...\" : \"Delete\"}\n </button>\n </>\n }\n >\n <p className=\"text-sm text-text-secondary\">\n Deleting a channel stops it from feeding new time into TimeCampus.\n Previously captured entries are kept.\n </p>\n </Modal>\n\n <ToastContainer toasts={toasts} onDismiss={dismissToast} />\n </PageLayout>\n );\n}\n"],"mappings":"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"}